From e1bfd8bbd30a667174a383c6b99f15a854a2caa5 Mon Sep 17 00:00:00 2001 From: jase100k Date: Thu, 7 Apr 2022 14:40:15 +1000 Subject: [PATCH 01/11] BIP85 - Derive your own child seed from a mnemonic --- requirements.txt | 1 + src/seedsigner/gui/screens/seed_screens.py | 90 ++++++++++++- src/seedsigner/models/seed.py | 32 ++++- src/seedsigner/views/seed_views.py | 140 +++++++++++++++++++-- 4 files changed, 246 insertions(+), 17 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1fb2f7b7..a09bab39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ RPi.GPIO==0.7.0 six==1.16.0 spidev==3.5 urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241 +bip85==0.2.0 diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index b86cbf17..a6414839 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -22,7 +22,6 @@ from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants - @dataclass class SeedMnemonicEntryScreen(BaseTopNavScreen): initial_letters: list = None @@ -1575,3 +1574,92 @@ class MultisigWalletDescriptorScreen(ButtonListScreen): auto_line_break=True, allow_text_overflow=True, )) + +@dataclass +class BIP85SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): + #num_words: int + seed: Seed = None + page_index: int = 0 + num_pages: int = 3 +# bip85_index: int = 0 +# bip85_num_words: int = 12 + is_bottom_list: bool = True + status_color: str = GUIConstants.DIRE_WARNING_COLOR + + + def __post_init__(self): + self.title = f"BIP85 Words: {self.page_index+1}/{self.num_pages}" + super().__post_init__() + + #index = 0 + #num_words = 12 + # Can only render 4 words per screen + words_per_page = 4 + mnemonic = self.seed.get_bip85_child(self.seed.bip85_index, self.seed.bip85_num_words).split() + # Slice the mnemonic to our current 4-word section + self.mnemonic = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] + self.body_x = 0 + self.body_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING / 2) + self.body_height = self.buttons[0].screen_y - self.body_y + + # Have to supersample the whole body since it's all at the small font size + supersampling_factor = 1 + font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, (GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2) * supersampling_factor) + + # Calc horizontal center based on longest word + max_word_width = 0 + for word in self.mnemonic: + (left, top, right, bottom) = font.getbbox(word, anchor="ls") + if right > max_word_width: + max_word_width = right + + # Measure the max digit height for the numbering boxes, from baseline + number_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE * supersampling_factor) + (left, top, right, bottom) = number_font.getbbox("24", anchor="ls") + number_height = -1 * top + number_width = right + number_box_width = number_width + int(GUIConstants.COMPONENT_PADDING/2 * supersampling_factor) + number_box_height = number_box_width + + number_box_x = int((self.canvas_width * supersampling_factor - number_box_width - GUIConstants.COMPONENT_PADDING*supersampling_factor - max_word_width))/2 + number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor + + # Set up our temp supersampled rendering surface + self.body_img = Image.new( + "RGB", + (self.canvas_width * supersampling_factor, self.body_height * supersampling_factor), + GUIConstants.BACKGROUND_COLOR + ) + draw = ImageDraw.Draw(self.body_img) + + for index, word in enumerate(self.mnemonic): + draw.rounded_rectangle( + (number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height), + fill="#202020", + radius=5 * supersampling_factor + ) + baseline_y = number_box_y + number_box_height - int((number_box_height - number_height)/2) + draw.text( + (number_box_x + int(number_box_width/2), baseline_y), + font=number_font, + text=str(self.page_index * words_per_page + index + 1), + fill="#0084ff", + anchor="ms" # Middle (centered), baSeline + ) + + # Now draw the word + draw.text( + (number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y), + font=font, + text=word, + fill=GUIConstants.BODY_FONT_COLOR, + anchor="ls", # Left, baSeline + ) + + number_box_y += number_box_height + (int(1.5*GUIConstants.COMPONENT_PADDING) * supersampling_factor) + + # Resize to target and sharpen final image + self.body_img = self.body_img.resize((self.canvas_width, self.body_height), Image.LANCZOS) + self.body_img = self.body_img.filter(ImageFilter.SHARPEN) + self.paste_images.append((self.body_img, (self.body_x, self.body_y))) + diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 43a2f4d8..88edcdc5 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -1,22 +1,22 @@ import unicodedata +import embit from binascii import hexlify from embit import bip39, bip32 from embit.networks import NETWORKS from typing import List +from bip85 import BIP85 +from bip85 import app from seedsigner.models.settings import SettingsConstants - - class InvalidSeedException(Exception): pass - - class Seed: def __init__(self, mnemonic: List[str] = None, + bip85_seed: List[str] = None, passphrase: str = "", wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None: self.wordlist_language_code = wordlist_language_code @@ -30,6 +30,9 @@ class Seed: self.seed_bytes: bytes = None self._generate_seed() + self._bip85_seed: List[str] = "" + self.bip85_index: int = 0 + self.bip85_num_words: int = 12 @staticmethod @@ -53,7 +56,6 @@ class Seed: def mnemonic_str(self) -> str: return " ".join(self._mnemonic) - @property def mnemonic_list(self) -> List[str]: return self._mnemonic @@ -68,6 +70,14 @@ class Seed: def mnemonic_display_list(self) -> List[str]: return unicodedata.normalize("NFC", " ".join(self._mnemonic)).split() + @property + def bip85_seed_display_list(self) -> List[str]: + return unicodedata.normalize("NFC", " ".join(self._bip85_seed)).split() + + #@property + #def bip85_seed(self): + # return self._bip85_seed + @property def passphrase(self): @@ -112,6 +122,18 @@ class Seed: xpub = xprv.to_public() return xpub + def get_bip85_child(self, bip85_index: int, bip85_num_words: int, wallet_path: str = '/', network: str = SettingsConstants.MAINNET): + bip85_seed: str = "" + bip85 = BIP85() + + passphrase = self._passphrase + language = 'english' + + seed = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) + xprv = embit.bip32.HDKey.from_seed(seed) + bip85_seed = app.bip39(xprv, language, bip85_num_words, bip85_index) + return bip85_seed + ### override operators def __eq__(self, other): if isinstance(other, Seed): diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 9fb12d43..4fdee54f 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -350,6 +350,7 @@ class SeedOptionsView(View): VERIFY_ADDRESS = "Verify Addr" EXPORT_XPUB = "Export Xpub" BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) + GENBIP85 = ("Gen BIP85 Child seed") DISCARD = ("Discard Seed", None, None, "red") button_data = [] @@ -367,7 +368,7 @@ class SeedOptionsView(View): if self.controller.psbt: if PSBTParser.has_matching_input_fingerprint(self.controller.psbt, self.seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): if self.controller.resume_main_flow and self.controller.resume_main_flow == Controller.FLOW__PSBT: - # Re-route us directly back to the start of the PSBT flow + # Re-route us directly back to the start of the PSBT flow self.controller.resume_main_flow = None self.controller.psbt_seed = self.seed return Destination(PSBTOverviewView, skip_current_view=True) @@ -382,6 +383,7 @@ class SeedOptionsView(View): if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: button_data.append(EXPORT_XPUB) + button_data.append(GENBIP85) button_data.append(BACKUP) button_data.append(DISCARD) @@ -415,6 +417,122 @@ class SeedOptionsView(View): elif button_data[selected_menu_num] == DISCARD: return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num}) + elif button_data[selected_menu_num] == GENBIP85: + return Destination(SeedExportBIP85View, view_args={"seed_num": self.seed_num}) + + +"""**************************************************************************** + Export BIP85 Child Flow +****************************************************************************""" +# This will ask user word number and display warning if neccessary before displaying the words +class SeedExportBIP85View(View): + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + self.num_words = 0 + + def run(self): + + WORDS_12 = "12 Words" + WORDS_24 = "24 Words" + button_data=[WORDS_12, WORDS_24] + + selected_menu_num = ButtonListScreen( + title="Generate BIP85 Seed", + button_data=button_data + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if button_data[selected_menu_num] == WORDS_12: + self.num_words = 12 + #return Destination( + # BIP85SeedWordsView, + # view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": 12}, + # skip_current_view=True, # Prevent going BACK to WarningViews + #) + + elif button_data[selected_menu_num] == WORDS_24: + self.num_words = 24 + #return Destination( + # BIP85SeedWordsView, + # view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": 24}, + # skip_current_view=True, # Prevent going BACK to WarningViews + #) + + destination = Destination( + BIP85SeedWordsView, + view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words}, + skip_current_view=True, # Prevent going BACK to WarningViews + ) + if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: + # Forward straight to showing the words + return destination + + selected_menu_num = DireWarningScreen( + text="""You must keep your seed words private & away from all online devices.""", + ).display() + + if selected_menu_num == 0: + # User clicked "I Understand" + return destination + + elif selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + + +class BIP85SeedWordsView(View): + def __init__(self, seed_num: int, num_words: int, page_index: int = 0): + super().__init__() + self.seed_num = seed_num + self.num_words = num_words + self.page_index = page_index + if self.seed_num is None: + self.seed = self.controller.storage.get_pending_seed() + else: + self.seed = self.controller.get_seed(self.seed_num) + + #Default to 12 world BIP85 Seed, need to fix this + self.num_pages=int(self.num_words/4) + + def run(self): + args = {"seed_num": self.seed_num, "page_index": self.page_index, "num_words": self.num_words} + + NEXT = "Next" + DONE = "Done" + + + button_data = [] + if self.page_index < self.num_pages - 1 or self.seed_num is None: + button_data.append(NEXT) + else: + button_data.append(DONE) + self.seed.bip85_num_words = self.num_words + + selected_menu_num = seed_screens.BIP85SeedWordsScreen( + seed=self.seed, + page_index=self.page_index, + num_pages=self.num_pages, + button_data=button_data, + + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if button_data[selected_menu_num] == NEXT: + if self.seed_num is None and self.page_index == self.num_pages - 1: + return Destination(SeedFinalizeView) + else: + return Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1, "num_words": self.num_words}) + + + elif button_data[selected_menu_num] == DONE: + # Must clear history to avoid BACK button returning to private info + return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) + class SeedBackupView(View): @@ -827,7 +945,7 @@ class SeedWordsView(View): class SeedWordsBackupTestPromptView(View): def __init__(self, seed_num: int): self.seed_num = seed_num - + def run(self): VERIFY = "Verify" @@ -861,7 +979,7 @@ class SeedWordsBackupTestView(View): self.confirmed_list = confirmed_list if not self.confirmed_list: self.confirmed_list = [] - + self.cur_index = cur_index @@ -870,7 +988,7 @@ class SeedWordsBackupTestView(View): self.cur_index = int(random.random() * len(self.mnemonic_list)) while self.cur_index in self.confirmed_list: self.cur_index = int(random.random() * len(self.mnemonic_list)) - + real_word = self.mnemonic_list[self.cur_index] fake_word1 = bip39.WORDLIST[int(random.random() * 2047)] fake_word2 = bip39.WORDLIST[int(random.random() * 2047)] @@ -895,7 +1013,7 @@ class SeedWordsBackupTestView(View): else: # Continue testing the remaining words return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list)) - + else: # Picked the WRONG WORD! return Destination( @@ -918,7 +1036,7 @@ class SeedWordsBackupTestMistakeView(View): self.wrong_word = wrong_word self.confirmed_list = confirmed_list - + def run(self): REVIEW = "Review Seed Words" RETRY = "Try Again" @@ -934,16 +1052,16 @@ class SeedWordsBackupTestMistakeView(View): if button_data[selected_menu_num] == REVIEW: return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num)) - + elif button_data[selected_menu_num] == RETRY: return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, cur_index=self.cur_index)) - + class SeedWordsBackupTestSuccessView(View): def __init__(self, seed_num: int): self.seed_num = seed_num - + def run(self): LargeIconStatusScreen( title="Backup Verified", @@ -1612,7 +1730,7 @@ class LoadMultisigWalletDescriptorView(View): if button_data[selected_menu_num] == SCAN: return Destination(ScanView) - + elif button_data[selected_menu_num] == CANCEL: if self.controller.resume_main_flow == Controller.FLOW__PSBT: return Destination(BackStackView) @@ -1631,7 +1749,7 @@ class MultisigWalletDescriptorView(View): fingerprints.append(fingerprint) policy = descriptor.brief_policy.split("multisig")[0].strip() - + RETURN = "Return to PSBT" VERIFY = "Verify Addr" OK = "OK" From bab9106f47896c556558cbd6de5fa098ee217c30 Mon Sep 17 00:00:00 2001 From: jase100k Date: Thu, 7 Apr 2022 18:28:31 +1000 Subject: [PATCH 02/11] BIP85 - enable child seed index selection --- src/seedsigner/gui/screens/seed_screens.py | 151 ++++++++++++++++++++- src/seedsigner/views/seed_views.py | 37 ++--- 2 files changed, 171 insertions(+), 17 deletions(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index a6414839..3043e138 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -21,7 +21,6 @@ from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddr from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants - @dataclass class SeedMnemonicEntryScreen(BaseTopNavScreen): initial_letters: list = None @@ -1575,6 +1574,156 @@ class MultisigWalletDescriptorScreen(ButtonListScreen): allow_text_overflow=True, )) + +@dataclass +class SeedExportBIP85GetIndexScreen(BaseTopNavScreen): + title: str = "BIP85 Seed Index" + bip85_index: str = "" + + def __post_init__(self): + super().__post_init__() + + # Set up the keyboard params + right_panel_buttons_width = 60 + hw_button_x = self.canvas_width - right_panel_buttons_width + GUIConstants.COMPONENT_PADDING + hw_button_y = int(self.canvas_height - GUIConstants.BUTTON_HEIGHT) / 2 + 60 + + keyboard_width = self.canvas_width - ( + GUIConstants.EDGE_PADDING + GUIConstants.COMPONENT_PADDING + right_panel_buttons_width - GUIConstants.COMPONENT_PADDING) + text_entry_display_y = self.top_nav.height + text_entry_display_height = 30 + + keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING + rows = 3 + self.keyboard_digits = Keyboard( + draw=self.renderer.draw, + charset="0123456789", + rows=rows, + cols=6, + rect=( + GUIConstants.EDGE_PADDING, + keyboard_start_y, + GUIConstants.EDGE_PADDING + keyboard_width, + keyboard_start_y + rows * GUIConstants.BUTTON_HEIGHT + (rows - 1) * 2 + ), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False + ) + self.keyboard_digits.set_selected_key(selected_letter="0") + + self.text_entry_display = TextEntryDisplay( + canvas=self.renderer.canvas, + rect=( + GUIConstants.EDGE_PADDING, + text_entry_display_y, + self.canvas_width - GUIConstants.EDGE_PADDING, + text_entry_display_y + text_entry_display_height + ), + cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR, + is_centered=False, + cur_text=''.join(self.bip85_index) + ) + + # Render the right button panel (only has a Key3 "Save" button) + self.exit_button = IconButton( + icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK, + icon_color=GUIConstants.SUCCESS_COLOR, + width=right_panel_buttons_width, + screen_x=hw_button_x, + screen_y=hw_button_y, + ) + + def _render(self): + super()._render() + + self.keyboard_digits.render_keys() + + self.exit_button.render() + self.text_entry_display.render(self.bip85_index) + self.renderer.show_image() + + def _run(self): + cursor_position = len(self.bip85_index) + + # Start the interactive update loop + while True: + input = self.hw_inputs.wait_for( + HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, + HardwareButtonsConstants.KEY3], + check_release=True, + release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3] + ) + + # Check our two possible exit conditions + if input == HardwareButtonsConstants.KEY3: + # Save! + self.exit_button.is_selected = True + self.exit_button.render() + self.renderer.show_image() + if len(self.bip85_index) > 0: + return self.bip85_index.strip() + + elif self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS: + # Prev button clicked; return empty string to signal cancel. + return self.top_nav.selected_button + + # Process normal input + if input in [HardwareButtonsConstants.KEY_UP, + HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: + # We're navigating off the previous button + self.top_nav.is_selected = False + self.top_nav.render_buttons() + + # Override the actual input w/an ENTER signal for the Keyboard + if input == HardwareButtonsConstants.KEY_DOWN: + input = Keyboard.ENTER_TOP + else: + input = Keyboard.ENTER_BOTTOM + elif input in [HardwareButtonsConstants.KEY_LEFT, + HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected: + # ignore + continue + + ret_val = self.keyboard_digits.update_from_input(input) + + # Now process the result from the keyboard + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.top_nav.is_selected = True + self.top_nav.render_buttons() + + elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS: + if ret_val == Keyboard.KEY_BACKSPACE["code"]: + if len(self.bip85_index) <= 2: + pass + elif cursor_position == len(self.bip85_index): + self.derivation_path = self.bip85_index[:-1] + cursor_position -= 1 + else: + self.bip85_index = self.bip85_index[:cursor_position - 1] + self.bip85_index[ + cursor_position:] + cursor_position -= 1 + + elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: + # User has locked in the current letter + if cursor_position == len(self.bip85_index): + self.bip85_index += ret_val + else: + self.bip85_index = self.bip85_index[:cursor_position] + ret_val + self.bip85_index[ + cursor_position:] + cursor_position += 1 + + elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN: + # Live joystick movement; haven't locked this new letter in yet. + # Leave current spot blank for now. Only update the active keyboard keys + # when a selection has been locked in (KEY_PRESS) or removed ("del"). + pass + + # Render the text entry display and cursor block + self.text_entry_display.render(self.bip85_index) + + self.renderer.show_image() + + @dataclass class BIP85SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): #num_words: int diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 4fdee54f..05bf1eec 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -430,6 +430,8 @@ class SeedExportBIP85View(View): super().__init__() self.seed_num = seed_num self.num_words = 0 + self.bip85_index = "" + def run(self): @@ -445,27 +447,28 @@ class SeedExportBIP85View(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) + ret = seed_screens.SeedExportBIP85GetIndexScreen( + ).display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + # ret should be the bip85_index let's convert to int + self.bip85_index = int(ret) + if button_data[selected_menu_num] == WORDS_12: self.num_words = 12 - #return Destination( - # BIP85SeedWordsView, - # view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": 12}, - # skip_current_view=True, # Prevent going BACK to WarningViews - #) elif button_data[selected_menu_num] == WORDS_24: self.num_words = 24 - #return Destination( - # BIP85SeedWordsView, - # view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": 24}, - # skip_current_view=True, # Prevent going BACK to WarningViews - #) destination = Destination( BIP85SeedWordsView, - view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words}, + view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words, + "bip85_index": self.bip85_index}, skip_current_view=True, # Prevent going BACK to WarningViews ) + if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: # Forward straight to showing the words return destination @@ -484,21 +487,21 @@ class SeedExportBIP85View(View): class BIP85SeedWordsView(View): - def __init__(self, seed_num: int, num_words: int, page_index: int = 0): + def __init__(self, seed_num: int, num_words: int, bip85_index: int, page_index: int = 0): super().__init__() self.seed_num = seed_num self.num_words = num_words self.page_index = page_index + self.bip85_index = bip85_index if self.seed_num is None: self.seed = self.controller.storage.get_pending_seed() else: self.seed = self.controller.get_seed(self.seed_num) - #Default to 12 world BIP85 Seed, need to fix this self.num_pages=int(self.num_words/4) def run(self): - args = {"seed_num": self.seed_num, "page_index": self.page_index, "num_words": self.num_words} + args = {"seed_num": self.seed_num, "page_index": self.page_index, "num_words": self.num_words, "bip85_index": self.bip85_index} NEXT = "Next" DONE = "Done" @@ -509,7 +512,10 @@ class BIP85SeedWordsView(View): button_data.append(NEXT) else: button_data.append(DONE) + + # Store the current word number and the index number selected self.seed.bip85_num_words = self.num_words + self.seed.bip85_index = self.bip85_index selected_menu_num = seed_screens.BIP85SeedWordsScreen( seed=self.seed, @@ -526,8 +532,7 @@ class BIP85SeedWordsView(View): if self.seed_num is None and self.page_index == self.num_pages - 1: return Destination(SeedFinalizeView) else: - return Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1, "num_words": self.num_words}) - + return Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1, "num_words": self.num_words, "bip85_index": self.bip85_index}) elif button_data[selected_menu_num] == DONE: # Must clear history to avoid BACK button returning to private info From 8625a9ec6153c2f3f5166f8c17568b32a06af86f Mon Sep 17 00:00:00 2001 From: jase100k Date: Thu, 7 Apr 2022 20:21:53 +1000 Subject: [PATCH 03/11] BIP85 - add setting to enable/disable feature to generate BIP85 child --- src/seedsigner/models/settings_definition.py | 7 +++++++ src/seedsigner/views/seed_views.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index f1f12af6..cfb5742b 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -149,6 +149,7 @@ class SettingsConstants: SETTING__SIG_TYPES = "sig_types" SETTING__SCRIPT_TYPES = "script_types" SETTING__XPUB_DETAILS = "xpub_details" + SETTING__GEN_BIP85 = "Generate BIP85" SETTING__PASSPHRASE = "passphrase" SETTING__CAMERA_ROTATION = "camera_rotation" SETTING__COMPACT_SEEDQR = "compact_seedqr" @@ -392,6 +393,12 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__GEN_BIP85, + display_name="Generate BIP85", + visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__ENABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__SIG_TYPES, display_name="Sig types", diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 05bf1eec..341b5f9c 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -382,8 +382,9 @@ class SeedOptionsView(View): if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: button_data.append(EXPORT_XPUB) + if self.settings.get_value(SettingsConstants.SETTING__GEN_BIP85) == SettingsConstants.OPTION__ENABLED: + button_data.append(GEN_BIP85) - button_data.append(GENBIP85) button_data.append(BACKUP) button_data.append(DISCARD) From ed5d8c38b5eafcf5613e7f283b1f2472fa7c4274 Mon Sep 17 00:00:00 2001 From: jase100k Date: Thu, 7 Apr 2022 20:21:53 +1000 Subject: [PATCH 04/11] BIP85 - add setting to enable/disable feature to generate BIP85 child --- src/seedsigner/views/seed_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 341b5f9c..496cba26 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -383,7 +383,7 @@ class SeedOptionsView(View): if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: button_data.append(EXPORT_XPUB) if self.settings.get_value(SettingsConstants.SETTING__GEN_BIP85) == SettingsConstants.OPTION__ENABLED: - button_data.append(GEN_BIP85) + button_data.append(GENBIP85) button_data.append(BACKUP) button_data.append(DISCARD) From e6a321f321a56884bcb6a961ee542684ae09d24f Mon Sep 17 00:00:00 2001 From: jase100k Date: Sun, 24 Apr 2022 01:32:11 +1000 Subject: [PATCH 05/11] BIP85 - Removed dependency for using bip85 reference library - Added bip85.py hmac_512 function embit didn't have it - Changed default setting to disable the BIP85 GEN function in seed menu, need to enable it in the settings --- requirements.txt | 1 - src/seedsigner/gui/screens/seed_screens.py | 2 +- src/seedsigner/helpers/bip85.py | 7 +++++ src/seedsigner/models/seed.py | 29 ++++++++++++-------- src/seedsigner/models/settings_definition.py | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 src/seedsigner/helpers/bip85.py diff --git a/requirements.txt b/requirements.txt index a09bab39..1fb2f7b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,3 @@ RPi.GPIO==0.7.0 six==1.16.0 spidev==3.5 urtypes @ git+https://github.com/jreesun/urtypes.git@e0d0db277ec2339650343eaf7b220fffb9233241 -bip85==0.2.0 diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 3043e138..cd9b5016 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1744,7 +1744,7 @@ class BIP85SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): #num_words = 12 # Can only render 4 words per screen words_per_page = 4 - mnemonic = self.seed.get_bip85_child(self.seed.bip85_index, self.seed.bip85_num_words).split() + mnemonic = self.seed.get_bip85_child_mnemonic(self.seed.bip85_index, self.seed.bip85_num_words).split() # Slice the mnemonic to our current 4-word section self.mnemonic = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] self.body_x = 0 diff --git a/src/seedsigner/helpers/bip85.py b/src/seedsigner/helpers/bip85.py new file mode 100644 index 00000000..5360829e --- /dev/null +++ b/src/seedsigner/helpers/bip85.py @@ -0,0 +1,7 @@ +import hashlib +import hmac + +def hmac_sha512(message_k): + return hmac.new(key=b'bip-entropy-from-k', msg=message_k, digestmod=hashlib.sha512).digest() + + diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 88edcdc5..219aff62 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -5,9 +5,8 @@ from binascii import hexlify from embit import bip39, bip32 from embit.networks import NETWORKS from typing import List -from bip85 import BIP85 -from bip85 import app +from seedsigner.helpers.bip85 import hmac_sha512 from seedsigner.models.settings import SettingsConstants class InvalidSeedException(Exception): @@ -121,20 +120,26 @@ class Seed: xprv = root.derive(wallet_path) xpub = xprv.to_public() return xpub - - def get_bip85_child(self, bip85_index: int, bip85_num_words: int, wallet_path: str = '/', network: str = SettingsConstants.MAINNET): - bip85_seed: str = "" - bip85 = BIP85() - - passphrase = self._passphrase - language = 'english' + # Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions + def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int, wallet_path: str = '/', + network: str = SettingsConstants.MAINNET): + passphrase = self._passphrase + # language = 'english' + # lang_code = 0 + # Need to add language later for path, defaults to English (0) + path = "m/83696968'/39'/0'/{bip85_num_words}'/{bip85_index}'".format(bip85_num_words=bip85_num_words, + bip85_index=bip85_index) seed = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) xprv = embit.bip32.HDKey.from_seed(seed) - bip85_seed = app.bip39(xprv, language, bip85_num_words, bip85_index) - return bip85_seed - ### override operators + # Derive k + xprv = xprv.derive(path) + entropy = hmac_sha512(xprv.secret) + width = round(bip85_num_words / 12 * 16) + return bip39.mnemonic_from_bytes(entropy[:width]) + + ### override operators def __eq__(self, other): if isinstance(other, Seed): return self.seed_bytes == other.seed_bytes diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index cfb5742b..7556a2a3 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -397,7 +397,7 @@ class SettingsDefinition: attr_name=SettingsConstants.SETTING__GEN_BIP85, display_name="Generate BIP85", visibility=SettingsConstants.VISIBILITY__ADVANCED, - default_value=SettingsConstants.OPTION__ENABLED), + default_value=SettingsConstants.OPTION__DISABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__SIG_TYPES, From 1330d67627bcc5eb5467c8f4b5f0e51fb4d5a1cd Mon Sep 17 00:00:00 2001 From: jase100k Date: Wed, 27 Apr 2022 20:04:51 +1000 Subject: [PATCH 06/11] BIP85 - Added test_bip85.py - Updates to get_bip85_child_mnemonic - Updates to BIP-85 naming used - Modified the flow for BIP-85 Child to use one display per function - fix to BIP85 Index input wasn't deleting the first two characters --- src/seedsigner/gui/screens/seed_screens.py | 8 +- src/seedsigner/models/seed.py | 10 +- src/seedsigner/models/settings_definition.py | 6 +- src/seedsigner/views/seed_views.py | 115 +++++++++++++++---- tests/test_bip85.py | 23 ++++ 5 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 tests/test_bip85.py diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index cd9b5016..c8a1930c 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1577,7 +1577,7 @@ class MultisigWalletDescriptorScreen(ButtonListScreen): @dataclass class SeedExportBIP85GetIndexScreen(BaseTopNavScreen): - title: str = "BIP85 Seed Index" + title: str = "BIP-85 Index" bip85_index: str = "" def __post_init__(self): @@ -1693,10 +1693,10 @@ class SeedExportBIP85GetIndexScreen(BaseTopNavScreen): elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS: if ret_val == Keyboard.KEY_BACKSPACE["code"]: - if len(self.bip85_index) <= 2: + if cursor_position == 0: pass elif cursor_position == len(self.bip85_index): - self.derivation_path = self.bip85_index[:-1] + self.bip85_index = self.bip85_index[:-1] cursor_position -= 1 else: self.bip85_index = self.bip85_index[:cursor_position - 1] + self.bip85_index[ @@ -1737,7 +1737,7 @@ class BIP85SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): def __post_init__(self): - self.title = f"BIP85 Words: {self.page_index+1}/{self.num_pages}" + self.title = f"BIP-85 Words: {self.page_index+1}/{self.num_pages}" super().__post_init__() #index = 0 diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 219aff62..3a9a87c3 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -122,19 +122,19 @@ class Seed: return xpub # Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions - def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int, wallet_path: str = '/', - network: str = SettingsConstants.MAINNET): + def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int): passphrase = self._passphrase # language = 'english' # lang_code = 0 # Need to add language later for path, defaults to English (0) path = "m/83696968'/39'/0'/{bip85_num_words}'/{bip85_index}'".format(bip85_num_words=bip85_num_words, bip85_index=bip85_index) - seed = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) - xprv = embit.bip32.HDKey.from_seed(seed) + #seed = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) + # xprv = embit.bip32.HDKey.from_seed(seed) + root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) # Derive k - xprv = xprv.derive(path) + xprv = root.derive(path) entropy = hmac_sha512(xprv.secret) width = round(bip85_num_words / 12 * 16) return bip39.mnemonic_from_bytes(entropy[:width]) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index 7556a2a3..c7903da5 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -149,7 +149,7 @@ class SettingsConstants: SETTING__SIG_TYPES = "sig_types" SETTING__SCRIPT_TYPES = "script_types" SETTING__XPUB_DETAILS = "xpub_details" - SETTING__GEN_BIP85 = "Generate BIP85" + SETTING__BIP85_CHILD_SEEDS = "BIP-85_child_seeds" SETTING__PASSPHRASE = "passphrase" SETTING__CAMERA_ROTATION = "camera_rotation" SETTING__COMPACT_SEEDQR = "compact_seedqr" @@ -394,8 +394,8 @@ class SettingsDefinition: default_value=SettingsConstants.OPTION__ENABLED), SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, - attr_name=SettingsConstants.SETTING__GEN_BIP85, - display_name="Generate BIP85", + attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS, + display_name="BIP-85 child seeds", visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 496cba26..6af9a51c 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -350,7 +350,7 @@ class SeedOptionsView(View): VERIFY_ADDRESS = "Verify Addr" EXPORT_XPUB = "Export Xpub" BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT) - GENBIP85 = ("Gen BIP85 Child seed") + BIP85_CHILD_SEED = "BIP-85 Child Seed" DISCARD = ("Discard Seed", None, None, "red") button_data = [] @@ -382,8 +382,8 @@ class SeedOptionsView(View): if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: button_data.append(EXPORT_XPUB) - if self.settings.get_value(SettingsConstants.SETTING__GEN_BIP85) == SettingsConstants.OPTION__ENABLED: - button_data.append(GENBIP85) + if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.append(BIP85_CHILD_SEED) button_data.append(BACKUP) button_data.append(DISCARD) @@ -418,36 +418,92 @@ class SeedOptionsView(View): elif button_data[selected_menu_num] == DISCARD: return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num}) - elif button_data[selected_menu_num] == GENBIP85: - return Destination(SeedExportBIP85View, view_args={"seed_num": self.seed_num}) + elif button_data[selected_menu_num] == BIP85_CHILD_SEED: + return Destination(BIP85ApplicationModeView, view_args={"seed_num": self.seed_num}) + """**************************************************************************** - Export BIP85 Child Flow -****************************************************************************""" -# This will ask user word number and display warning if neccessary before displaying the words -class SeedExportBIP85View(View): + BIP85 - Derive child mnemonic (seed) flow +**************************************************************************** +* Ask the user the application type as defined in the BIP0085 spec. +* Currently only Word mode of 12, 24 words. +* Possible future additions are +* WIF (HDSEED) +* XPRV (BIP32)""" +class BIP85ApplicationModeView(View): def __init__(self, seed_num: int): super().__init__() self.seed_num = seed_num self.num_words = 0 - self.bip85_index = "" - + self.bip85_index = 0 + self.bip85_app_no = None def run(self): WORDS_12 = "12 Words" WORDS_24 = "24 Words" - button_data=[WORDS_12, WORDS_24] + # Need to setup width as 32 + WIF = "WIF" + #Need to setup width as 64 + XPRV = "XPRV" + + # Future enhancement to display WIF (HD-SEED) and XPRV (Bip32) + #button_data=[WORDS_12, WORDS_24, WIF, XPRV] + button_data = [WORDS_12, WORDS_24] selected_menu_num = ButtonListScreen( - title="Generate BIP85 Seed", + title="BIP-85 Num Words", button_data=button_data ).display() if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) + if button_data[selected_menu_num] == WORDS_12: + self.bip85_app_no = "words" + self.num_words = 12 + elif button_data[selected_menu_num] == WORDS_24: + self.bip85_app_no = "words" + self.num_words = 24 + elif button_data[selected_menu_num] == WIF: + print("WIF") + self.bip85_app_no = "wif" + return(Destination(NotYetImplementedView)) + elif button_data[selected_menu_num] == XPRV: + self.bip85_app_no = "xprv" + return(Destination(NotYetImplementedView)) + + destination = Destination( + BIP85ChildSeedIndexView, + view_args={"seed_num": self.seed_num, "num_words": self.num_words, "bip85_app_no" : self.bip85_app_no, + "bip85_index": self.bip85_index}) + #skip_current_view=True, # Prevent going BACK to WarningViews + + return(destination) + +## + + +# View to retrieve the derived seed index +class BIP85ChildSeedIndexView(View): + def __init__(self, seed_num: int, num_words: int, bip85_app_no: str, bip85_index: int): + super().__init__() + self.bip85_app_no = bip85_app_no + self.seed_num = seed_num + self.num_words = num_words + self.bip85_index = bip85_index + #if self.seed_num is None: + # self.seed = self.controller.storage.get_pending_seed() + #else: + # self.seed = self.controller.get_seed(self.seed_num) + + #self.num_pages=int(self.num_words/4) + + def run(self): + args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_app_no" : self.bip85_app_no, "bip85_index": self.bip85_index} + + # Change this later to use the generic Screen input keyboard ret = seed_screens.SeedExportBIP85GetIndexScreen( ).display() @@ -457,17 +513,31 @@ class SeedExportBIP85View(View): # ret should be the bip85_index let's convert to int self.bip85_index = int(ret) - if button_data[selected_menu_num] == WORDS_12: - self.num_words = 12 + return Destination( + BIP85ChildSeedWarningView, + view_args={"seed_num": self.seed_num, "num_words": self.num_words, + "bip85_index": self.bip85_index} + ) - elif button_data[selected_menu_num] == WORDS_24: - self.num_words = 24 +class BIP85ChildSeedWarningView(View): + def __init__(self, seed_num: int, num_words: int, bip85_index: int): + super().__init__() + self.seed_num = seed_num + self.num_words = num_words + self.bip85_index = bip85_index + # if self.seed_num is None: + # self.seed = self.controller.storage.get_pending_seed() + # else: + # self.seed = self.controller.get_seed(self.seed_num) - destination = Destination( - BIP85SeedWordsView, - view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words, - "bip85_index": self.bip85_index}, - skip_current_view=True, # Prevent going BACK to WarningViews + # self.num_pages=int(self.num_words/4) + + def run(self): + args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_index": self.bip85_index} + + ## + destination = Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words, + "bip85_index": self.bip85_index}, skip_current_view=True, # Prevent going BACK to WarningViews ) if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: @@ -486,7 +556,6 @@ class SeedExportBIP85View(View): return Destination(BackStackView) - class BIP85SeedWordsView(View): def __init__(self, seed_num: int, num_words: int, bip85_index: int, page_index: int = 0): super().__init__() diff --git a/tests/test_bip85.py b/tests/test_bip85.py new file mode 100644 index 00000000..c9f67a31 --- /dev/null +++ b/tests/test_bip85.py @@ -0,0 +1,23 @@ +import pytest +from mock import MagicMock +from seedsigner.models import Seed +from embit import bip39 + +from seedsigner.models.settings import SettingsConstants + + + +def test_derive_child_mnemonic(): + + expected = "unusual topic foot figure pulp target glimpse core electric spot neglect fame" + seed = Seed(mnemonic="resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious".split()) + + actual = seed.get_bip85_child_mnemonic(0, 12) + assert actual == expected + + + expected = "imitate post very mandate retreat prevent tiny snow fetch canvas town shrug fix food summer library symptom occur slam style cruise wolf phone key" + seed = Seed(mnemonic="resource timber firm banner horror pupil frozen main pear direct pioneer broken grid core insane begin sister pony end debate task silk empty curious".split()) + + actual = seed.get_bip85_child_mnemonic(0, 24) + assert actual == expected From ba49a52bc0df3ef13e99e4c0a6e08eb85ebede0d Mon Sep 17 00:00:00 2001 From: jase100k Date: Wed, 27 Apr 2022 22:00:29 +1000 Subject: [PATCH 07/11] BIP85 - Updates to get_bip85_child_mnemonic --- src/seedsigner/models/seed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 3a9a87c3..d85bd31a 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -122,7 +122,7 @@ class Seed: return xpub # Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions - def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int): + def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int, network: str = SettingsConstants.MAINNET): passphrase = self._passphrase # language = 'english' # lang_code = 0 From 1f3aee84eedff2433ba0fb4156125354003ac02a Mon Sep 17 00:00:00 2001 From: jase100k Date: Thu, 7 Apr 2022 14:40:15 +1000 Subject: [PATCH 08/11] BIP85 - Derive your own child seed from mnemonic --- src/seedsigner/gui/screens/seed_screens.py | 245 +-------------------- src/seedsigner/models/seed.py | 27 +-- src/seedsigner/views/seed_views.py | 64 ++---- 3 files changed, 38 insertions(+), 298 deletions(-) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index e9d4f3ac..0fa6f5cc 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -21,6 +21,8 @@ from ..components import (Button, FontAwesomeIconConstants, Fonts, FormattedAddr from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay from seedsigner.hardware.buttons import HardwareButtons, HardwareButtonsConstants + + @dataclass class SeedMnemonicEntryScreen(BaseTopNavScreen): initial_letters: list = None @@ -1438,241 +1440,16 @@ class MultisigWalletDescriptorScreen(ButtonListScreen): allow_text_overflow=True, )) - @dataclass -class SeedExportBIP85GetIndexScreen(BaseTopNavScreen): - title: str = "BIP-85 Index" - bip85_index: str = "" - +class BIP85SeedIndexScreen(KeyboardScreen): def __post_init__(self): - super().__post_init__() + self.title = "BIP-85 Index" + self.user_input = "" - # Set up the keyboard params - right_panel_buttons_width = 60 - hw_button_x = self.canvas_width - right_panel_buttons_width + GUIConstants.COMPONENT_PADDING - hw_button_y = int(self.canvas_height - GUIConstants.BUTTON_HEIGHT) / 2 + 60 - - keyboard_width = self.canvas_width - ( - GUIConstants.EDGE_PADDING + GUIConstants.COMPONENT_PADDING + right_panel_buttons_width - GUIConstants.COMPONENT_PADDING) - text_entry_display_y = self.top_nav.height - text_entry_display_height = 30 - - keyboard_start_y = text_entry_display_y + text_entry_display_height + GUIConstants.COMPONENT_PADDING - rows = 3 - self.keyboard_digits = Keyboard( - draw=self.renderer.draw, - charset="0123456789", - rows=rows, - cols=6, - rect=( - GUIConstants.EDGE_PADDING, - keyboard_start_y, - GUIConstants.EDGE_PADDING + keyboard_width, - keyboard_start_y + rows * GUIConstants.BUTTON_HEIGHT + (rows - 1) * 2 - ), - auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], - render_now=False - ) - self.keyboard_digits.set_selected_key(selected_letter="0") - - self.text_entry_display = TextEntryDisplay( - canvas=self.renderer.canvas, - rect=( - GUIConstants.EDGE_PADDING, - text_entry_display_y, - self.canvas_width - GUIConstants.EDGE_PADDING, - text_entry_display_y + text_entry_display_height - ), - cursor_mode=TextEntryDisplay.CURSOR_MODE__BAR, - is_centered=False, - cur_text=''.join(self.bip85_index) - ) - - # Render the right button panel (only has a Key3 "Save" button) - self.exit_button = IconButton( - icon_name=FontAwesomeIconConstants.SOLID_CIRCLE_CHECK, - icon_color=GUIConstants.SUCCESS_COLOR, - width=right_panel_buttons_width, - screen_x=hw_button_x, - screen_y=hw_button_y, - ) - - def _render(self): - super()._render() - - self.keyboard_digits.render_keys() - - self.exit_button.render() - self.text_entry_display.render(self.bip85_index) - self.renderer.show_image() - - def _run(self): - cursor_position = len(self.bip85_index) - - # Start the interactive update loop - while True: - input = self.hw_inputs.wait_for( - HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, - HardwareButtonsConstants.KEY3], - check_release=True, - release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3] - ) - - # Check our two possible exit conditions - if input == HardwareButtonsConstants.KEY3: - # Save! - self.exit_button.is_selected = True - self.exit_button.render() - self.renderer.show_image() - if len(self.bip85_index) > 0: - return self.bip85_index.strip() - - elif self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS: - # Prev button clicked; return empty string to signal cancel. - return self.top_nav.selected_button - - # Process normal input - if input in [HardwareButtonsConstants.KEY_UP, - HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: - # We're navigating off the previous button - self.top_nav.is_selected = False - self.top_nav.render_buttons() - - # Override the actual input w/an ENTER signal for the Keyboard - if input == HardwareButtonsConstants.KEY_DOWN: - input = Keyboard.ENTER_TOP - else: - input = Keyboard.ENTER_BOTTOM - elif input in [HardwareButtonsConstants.KEY_LEFT, - HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected: - # ignore - continue - - ret_val = self.keyboard_digits.update_from_input(input) - - # Now process the result from the keyboard - if ret_val in Keyboard.EXIT_DIRECTIONS: - self.top_nav.is_selected = True - self.top_nav.render_buttons() - - elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS: - if ret_val == Keyboard.KEY_BACKSPACE["code"]: - if cursor_position == 0: - pass - elif cursor_position == len(self.bip85_index): - self.bip85_index = self.bip85_index[:-1] - cursor_position -= 1 - else: - self.bip85_index = self.bip85_index[:cursor_position - 1] + self.bip85_index[ - cursor_position:] - cursor_position -= 1 - - elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: - # User has locked in the current letter - if cursor_position == len(self.bip85_index): - self.bip85_index += ret_val - else: - self.bip85_index = self.bip85_index[:cursor_position] + ret_val + self.bip85_index[ - cursor_position:] - cursor_position += 1 - - elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN: - # Live joystick movement; haven't locked this new letter in yet. - # Leave current spot blank for now. Only update the active keyboard keys - # when a selection has been locked in (KEY_PRESS) or removed ("del"). - pass - - # Render the text entry display and cursor block - self.text_entry_display.render(self.bip85_index) - - self.renderer.show_image() - - -@dataclass -class BIP85SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): - #num_words: int - seed: Seed = None - page_index: int = 0 - num_pages: int = 3 -# bip85_index: int = 0 -# bip85_num_words: int = 12 - is_bottom_list: bool = True - status_color: str = GUIConstants.DIRE_WARNING_COLOR - - - def __post_init__(self): - self.title = f"BIP-85 Words: {self.page_index+1}/{self.num_pages}" - super().__post_init__() - - #index = 0 - #num_words = 12 - # Can only render 4 words per screen - words_per_page = 4 - mnemonic = self.seed.get_bip85_child_mnemonic(self.seed.bip85_index, self.seed.bip85_num_words).split() - # Slice the mnemonic to our current 4-word section - self.mnemonic = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] - self.body_x = 0 - self.body_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING / 2) - self.body_height = self.buttons[0].screen_y - self.body_y - - # Have to supersample the whole body since it's all at the small font size - supersampling_factor = 1 - font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, (GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2) * supersampling_factor) - - # Calc horizontal center based on longest word - max_word_width = 0 - for word in self.mnemonic: - (left, top, right, bottom) = font.getbbox(word, anchor="ls") - if right > max_word_width: - max_word_width = right - - # Measure the max digit height for the numbering boxes, from baseline - number_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE * supersampling_factor) - (left, top, right, bottom) = number_font.getbbox("24", anchor="ls") - number_height = -1 * top - number_width = right - number_box_width = number_width + int(GUIConstants.COMPONENT_PADDING/2 * supersampling_factor) - number_box_height = number_box_width - - number_box_x = int((self.canvas_width * supersampling_factor - number_box_width - GUIConstants.COMPONENT_PADDING*supersampling_factor - max_word_width))/2 - number_box_y = GUIConstants.COMPONENT_PADDING * supersampling_factor - - # Set up our temp supersampled rendering surface - self.body_img = Image.new( - "RGB", - (self.canvas_width * supersampling_factor, self.body_height * supersampling_factor), - GUIConstants.BACKGROUND_COLOR - ) - draw = ImageDraw.Draw(self.body_img) - - for index, word in enumerate(self.mnemonic): - draw.rounded_rectangle( - (number_box_x, number_box_y, number_box_x + number_box_width, number_box_y + number_box_height), - fill="#202020", - radius=5 * supersampling_factor - ) - baseline_y = number_box_y + number_box_height - int((number_box_height - number_height)/2) - draw.text( - (number_box_x + int(number_box_width/2), baseline_y), - font=number_font, - text=str(self.page_index * words_per_page + index + 1), - fill="#0084ff", - anchor="ms" # Middle (centered), baSeline - ) - - # Now draw the word - draw.text( - (number_box_x + number_box_width + (GUIConstants.COMPONENT_PADDING * supersampling_factor), baseline_y), - font=font, - text=word, - fill=GUIConstants.BODY_FONT_COLOR, - anchor="ls", # Left, baSeline - ) - - number_box_y += number_box_height + (int(1.5*GUIConstants.COMPONENT_PADDING) * supersampling_factor) - - # Resize to target and sharpen final image - self.body_img = self.body_img.resize((self.canvas_width, self.body_height), Image.LANCZOS) - self.body_img = self.body_img.filter(ImageFilter.SHARPEN) - self.paste_images.append((self.body_img, (self.body_x, self.body_y))) + # Specify the keys in the keyboard + self.rows = 3 + self.cols = 6 + self.keys_charset = "0123456789" + self.show_save_button = True + super().__post_init__() \ No newline at end of file diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index d85bd31a..96cba473 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -1,5 +1,4 @@ import unicodedata -import embit from binascii import hexlify from embit import bip39, bip32 @@ -9,13 +8,16 @@ from typing import List from seedsigner.helpers.bip85 import hmac_sha512 from seedsigner.models.settings import SettingsConstants + + class InvalidSeedException(Exception): pass + + class Seed: def __init__(self, mnemonic: List[str] = None, - bip85_seed: List[str] = None, passphrase: str = "", wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None: self.wordlist_language_code = wordlist_language_code @@ -29,9 +31,6 @@ class Seed: self.seed_bytes: bytes = None self._generate_seed() - self._bip85_seed: List[str] = "" - self.bip85_index: int = 0 - self.bip85_num_words: int = 12 @staticmethod @@ -55,6 +54,7 @@ class Seed: def mnemonic_str(self) -> str: return " ".join(self._mnemonic) + @property def mnemonic_list(self) -> List[str]: return self._mnemonic @@ -69,14 +69,6 @@ class Seed: def mnemonic_display_list(self) -> List[str]: return unicodedata.normalize("NFC", " ".join(self._mnemonic)).split() - @property - def bip85_seed_display_list(self) -> List[str]: - return unicodedata.normalize("NFC", " ".join(self._bip85_seed)).split() - - #@property - #def bip85_seed(self): - # return self._bip85_seed - @property def passphrase(self): @@ -114,23 +106,20 @@ class Seed: def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str: root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) return hexlify(root.child(0).fingerprint).decode('utf-8') - + def get_xpub(self, wallet_path: str = '/', network: str = SettingsConstants.MAINNET): root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) xprv = root.derive(wallet_path) xpub = xprv.to_public() return xpub + # Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int, network: str = SettingsConstants.MAINNET): passphrase = self._passphrase - # language = 'english' - # lang_code = 0 - # Need to add language later for path, defaults to English (0) + # TODO: Support other bip-39 wordlist languages! path = "m/83696968'/39'/0'/{bip85_num_words}'/{bip85_index}'".format(bip85_num_words=bip85_num_words, bip85_index=bip85_index) - #seed = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist) - # xprv = embit.bip32.HDKey.from_seed(seed) root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) # Derive k diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 8a700997..b7997f45 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -441,15 +441,10 @@ class BIP85ApplicationModeView(View): def run(self): + # Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)? WORDS_12 = "12 Words" WORDS_24 = "24 Words" - # Need to setup width as 32 - WIF = "WIF" - #Need to setup width as 64 - XPRV = "XPRV" - # Future enhancement to display WIF (HD-SEED) and XPRV (Bip32) - #button_data=[WORDS_12, WORDS_24, WIF, XPRV] button_data = [WORDS_12, WORDS_24] selected_menu_num = ButtonListScreen( @@ -460,51 +455,28 @@ class BIP85ApplicationModeView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == WORDS_12: - self.bip85_app_no = "words" - self.num_words = 12 - elif button_data[selected_menu_num] == WORDS_24: - self.bip85_app_no = "words" - self.num_words = 24 - elif button_data[selected_menu_num] == WIF: - print("WIF") - self.bip85_app_no = "wif" - return(Destination(NotYetImplementedView)) - elif button_data[selected_menu_num] == XPRV: - self.bip85_app_no = "xprv" - return(Destination(NotYetImplementedView)) destination = Destination( BIP85ChildSeedIndexView, - view_args={"seed_num": self.seed_num, "num_words": self.num_words, "bip85_app_no" : self.bip85_app_no, + view_args={"seed_num": self.seed_num, "num_words": self.num_words, "bip85_index": self.bip85_index}) - #skip_current_view=True, # Prevent going BACK to WarningViews return(destination) -## - # View to retrieve the derived seed index class BIP85ChildSeedIndexView(View): - def __init__(self, seed_num: int, num_words: int, bip85_app_no: str, bip85_index: int): + def __init__(self, seed_num: int, num_words: int, bip85_index: int): super().__init__() - self.bip85_app_no = bip85_app_no self.seed_num = seed_num self.num_words = num_words self.bip85_index = bip85_index - #if self.seed_num is None: - # self.seed = self.controller.storage.get_pending_seed() - #else: - # self.seed = self.controller.get_seed(self.seed_num) - - #self.num_pages=int(self.num_words/4) def run(self): - args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_app_no" : self.bip85_app_no, "bip85_index": self.bip85_index} + args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_index": self.bip85_index} # Change this later to use the generic Screen input keyboard - ret = seed_screens.SeedExportBIP85GetIndexScreen( + ret = seed_screens.BIP85SeedIndexScreen( ).display() if ret == RET_CODE__BACK_BUTTON: @@ -525,17 +497,11 @@ class BIP85ChildSeedWarningView(View): self.seed_num = seed_num self.num_words = num_words self.bip85_index = bip85_index - # if self.seed_num is None: - # self.seed = self.controller.storage.get_pending_seed() - # else: - # self.seed = self.controller.get_seed(self.seed_num) - - # self.num_pages=int(self.num_words/4) def run(self): args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_index": self.bip85_index} - ## + destination = Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words, "bip85_index": self.bip85_index}, skip_current_view=True, # Prevent going BACK to WarningViews ) @@ -576,6 +542,10 @@ class BIP85SeedWordsView(View): NEXT = "Next" DONE = "Done" + words_per_page = 4 # TODO: eventually make this configurable for bigger screens? + + mnemonic = self.seed.get_bip85_child_mnemonic(self.bip85_index, self.num_words).split() + words = mnemonic[self.page_index * words_per_page:(self.page_index + 1) * words_per_page] button_data = [] if self.page_index < self.num_pages - 1 or self.seed_num is None: @@ -583,12 +553,16 @@ class BIP85SeedWordsView(View): else: button_data.append(DONE) - # Store the current word number and the index number selected - self.seed.bip85_num_words = self.num_words - self.seed.bip85_index = self.bip85_index - selected_menu_num = seed_screens.BIP85SeedWordsScreen( - seed=self.seed, + button_data = [] + if self.page_index < self.num_pages - 1 or self.seed_num is None: + button_data.append(NEXT) + else: + button_data.append(DONE) + + selected_menu_num = seed_screens.SeedWordsScreen( + title=f"BIP-85: Words: {self.page_index + 1}/{self.num_pages}", + words=words, page_index=self.page_index, num_pages=self.num_pages, button_data=button_data, From c7f150630cdca2041e29abff5b8de8b9bbecb5a1 Mon Sep 17 00:00:00 2001 From: jase100k Date: Wed, 11 May 2022 18:55:34 +1000 Subject: [PATCH 09/11] BIP85 - remove bip85.py comments added in get_bip85_child_mnemonic() --- src/seedsigner/helpers/bip85.py | 7 ------- src/seedsigner/models/seed.py | 28 +++++++++++++++++++++------- src/seedsigner/views/seed_views.py | 10 +++++++--- 3 files changed, 28 insertions(+), 17 deletions(-) delete mode 100644 src/seedsigner/helpers/bip85.py diff --git a/src/seedsigner/helpers/bip85.py b/src/seedsigner/helpers/bip85.py deleted file mode 100644 index 5360829e..00000000 --- a/src/seedsigner/helpers/bip85.py +++ /dev/null @@ -1,7 +0,0 @@ -import hashlib -import hmac - -def hmac_sha512(message_k): - return hmac.new(key=b'bip-entropy-from-k', msg=message_k, digestmod=hashlib.sha512).digest() - - diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 96cba473..06498e24 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -5,9 +5,10 @@ from embit import bip39, bip32 from embit.networks import NETWORKS from typing import List -from seedsigner.helpers.bip85 import hmac_sha512 from seedsigner.models.settings import SettingsConstants +import hashlib +import hmac class InvalidSeedException(Exception): @@ -116,16 +117,29 @@ class Seed: # Derives a BIP85 mnemonic (seed word) from the master seed words using embit functions def get_bip85_child_mnemonic(self, bip85_index: int, bip85_num_words: int, network: str = SettingsConstants.MAINNET): - passphrase = self._passphrase - # TODO: Support other bip-39 wordlist languages! - path = "m/83696968'/39'/0'/{bip85_num_words}'/{bip85_index}'".format(bip85_num_words=bip85_num_words, - bip85_index=bip85_index) + + # Calculate the master bip32 root key from the parents bip39 seed_bytes (the mnemonic entropy) root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"]) - # Derive k + # TODO: Support other bip-39 wordlist languages! + # As per the BIP85 spec, 39 is the application number used for bip39 mnemonic, + # we use the selected word count and the index to form the full path. + path = "m/83696968'/39'/0'/{bip85_num_words}'/{bip85_index}'".format( + bip85_num_words=bip85_num_words, + bip85_index=bip85_index) + + # Derive the child xprv (HDKey) using the path format defined above xprv = root.derive(path) - entropy = hmac_sha512(xprv.secret) + + # The xprv.secret plus the BIP85 key is hashed together using hmac sha512. + entropy = hmac.new(key=b'bip-entropy-from-k', msg=xprv.secret, digestmod=hashlib.sha512).digest() + + # Calculate number of bytes to retain for the entropy + # 24 words the width is 32bytes (256bits) + # 12 words the width is 16bytes (128bits) width = round(bip85_num_words / 12 * 16) + + # Return the derived BIP85 child mnemonic using the truncated derived entropy return bip39.mnemonic_from_bytes(entropy[:width]) ### override operators diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index b7997f45..0d74bf4a 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -455,6 +455,10 @@ class BIP85ApplicationModeView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) + if button_data[selected_menu_num] == WORDS_12: + self.num_words = 12 + elif button_data[selected_menu_num] == WORDS_24: + self.num_words = 24 destination = Destination( BIP85ChildSeedIndexView, @@ -573,9 +577,9 @@ class BIP85SeedWordsView(View): return Destination(BackStackView) if button_data[selected_menu_num] == NEXT: - if self.seed_num is None and self.page_index == self.num_pages - 1: - return Destination(SeedFinalizeView) - else: +# if self.seed_num is None and self.page_index == self.num_pages - 1: +# return Destination(SeedFinalizeView) +# else: return Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1, "num_words": self.num_words, "bip85_index": self.bip85_index}) elif button_data[selected_menu_num] == DONE: From 901e4f7513e6d86f04a45359bdb56c12b6d46b15 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 9 Nov 2022 11:23:29 -0800 Subject: [PATCH 10/11] DRY optimizations, support BackupTest flow --- src/seedsigner/gui/screens/screen.py | 7 +- src/seedsigner/gui/screens/seed_screens.py | 4 +- src/seedsigner/models/settings_definition.py | 14 +- src/seedsigner/views/seed_views.py | 328 ++++++++----------- 4 files changed, 156 insertions(+), 197 deletions(-) diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index debaae45..95bba2f0 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1003,14 +1003,17 @@ class KeyboardScreen(BaseTopNavScreen): elif self.show_save_button and input == HardwareButtonsConstants.KEY3: # Save! + if len(self.user_input) == 0: + # Don't try to submit zero input + continue + # First show the save button reacting to the click self.save_button.is_selected = True self.save_button.render() self.renderer.show_image() # Then return the input to the View - if len(self.user_input) > 0: - return self.user_input.strip() + return self.user_input.strip() # Process normal input if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected: diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 0fa6f5cc..c33127c5 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -1441,14 +1441,14 @@ class MultisigWalletDescriptorScreen(ButtonListScreen): )) @dataclass -class BIP85SeedIndexScreen(KeyboardScreen): +class SeedBIP85SelectChildIndexScreen(KeyboardScreen): def __post_init__(self): self.title = "BIP-85 Index" self.user_input = "" # Specify the keys in the keyboard self.rows = 3 - self.cols = 6 + self.cols = 5 self.keys_charset = "0123456789" self.show_save_button = True diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index c7903da5..b5879b53 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -149,10 +149,10 @@ class SettingsConstants: SETTING__SIG_TYPES = "sig_types" SETTING__SCRIPT_TYPES = "script_types" SETTING__XPUB_DETAILS = "xpub_details" - SETTING__BIP85_CHILD_SEEDS = "BIP-85_child_seeds" SETTING__PASSPHRASE = "passphrase" SETTING__CAMERA_ROTATION = "camera_rotation" SETTING__COMPACT_SEEDQR = "compact_seedqr" + SETTING__BIP85_CHILD_SEEDS = "bip85_child_seeds" SETTING__PRIVACY_WARNINGS = "privacy_warnings" SETTING__DIRE_WARNINGS = "dire_warnings" SETTING__PARTNER_LOGOS = "partner_logos" @@ -393,12 +393,6 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__ENABLED), - SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, - attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS, - display_name="BIP-85 child seeds", - visibility=SettingsConstants.VISIBILITY__ADVANCED, - default_value=SettingsConstants.OPTION__DISABLED), - SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__SIG_TYPES, display_name="Sig types", @@ -443,6 +437,12 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS, + display_name="BIP-85 child seeds", + visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__DISABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS, display_name="Show privacy warnings", diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 0d74bf4a..f08d98f4 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -382,10 +382,12 @@ class SeedOptionsView(View): if self.settings.get_value(SettingsConstants.SETTING__XPUB_EXPORT) == SettingsConstants.OPTION__ENABLED: button_data.append(EXPORT_XPUB) + + button_data.append(BACKUP) + if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED: button_data.append(BIP85_CHILD_SEED) - button_data.append(BACKUP) button_data.append(DISCARD) selected_menu_num = seed_screens.SeedOptionsScreen( @@ -415,177 +417,12 @@ class SeedOptionsView(View): elif button_data[selected_menu_num] == BACKUP: return Destination(SeedBackupView, view_args={"seed_num": self.seed_num}) + elif button_data[selected_menu_num] == BIP85_CHILD_SEED: + return Destination(SeedBIP85ApplicationModeView, view_args={"seed_num": self.seed_num}) + elif button_data[selected_menu_num] == DISCARD: return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num}) - elif button_data[selected_menu_num] == BIP85_CHILD_SEED: - return Destination(BIP85ApplicationModeView, view_args={"seed_num": self.seed_num}) - - - -"""**************************************************************************** - BIP85 - Derive child mnemonic (seed) flow -**************************************************************************** -* Ask the user the application type as defined in the BIP0085 spec. -* Currently only Word mode of 12, 24 words. -* Possible future additions are -* WIF (HDSEED) -* XPRV (BIP32)""" -class BIP85ApplicationModeView(View): - def __init__(self, seed_num: int): - super().__init__() - self.seed_num = seed_num - self.num_words = 0 - self.bip85_index = 0 - self.bip85_app_no = None - - def run(self): - - # Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)? - WORDS_12 = "12 Words" - WORDS_24 = "24 Words" - - button_data = [WORDS_12, WORDS_24] - - selected_menu_num = ButtonListScreen( - title="BIP-85 Num Words", - button_data=button_data - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - if button_data[selected_menu_num] == WORDS_12: - self.num_words = 12 - elif button_data[selected_menu_num] == WORDS_24: - self.num_words = 24 - - destination = Destination( - BIP85ChildSeedIndexView, - view_args={"seed_num": self.seed_num, "num_words": self.num_words, - "bip85_index": self.bip85_index}) - - return(destination) - - -# View to retrieve the derived seed index -class BIP85ChildSeedIndexView(View): - def __init__(self, seed_num: int, num_words: int, bip85_index: int): - super().__init__() - self.seed_num = seed_num - self.num_words = num_words - self.bip85_index = bip85_index - - def run(self): - args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_index": self.bip85_index} - - # Change this later to use the generic Screen input keyboard - ret = seed_screens.BIP85SeedIndexScreen( - ).display() - - if ret == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - # ret should be the bip85_index let's convert to int - self.bip85_index = int(ret) - - return Destination( - BIP85ChildSeedWarningView, - view_args={"seed_num": self.seed_num, "num_words": self.num_words, - "bip85_index": self.bip85_index} - ) - -class BIP85ChildSeedWarningView(View): - def __init__(self, seed_num: int, num_words: int, bip85_index: int): - super().__init__() - self.seed_num = seed_num - self.num_words = num_words - self.bip85_index = bip85_index - - def run(self): - args = {"seed_num": self.seed_num, "num_words": self.num_words, "bip85_index": self.bip85_index} - - - destination = Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0, "num_words": self.num_words, - "bip85_index": self.bip85_index}, skip_current_view=True, # Prevent going BACK to WarningViews - ) - - if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: - # Forward straight to showing the words - return destination - - selected_menu_num = DireWarningScreen( - text="""You must keep your seed words private & away from all online devices.""", - ).display() - - if selected_menu_num == 0: - # User clicked "I Understand" - return destination - - elif selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - -class BIP85SeedWordsView(View): - def __init__(self, seed_num: int, num_words: int, bip85_index: int, page_index: int = 0): - super().__init__() - self.seed_num = seed_num - self.num_words = num_words - self.page_index = page_index - self.bip85_index = bip85_index - if self.seed_num is None: - self.seed = self.controller.storage.get_pending_seed() - else: - self.seed = self.controller.get_seed(self.seed_num) - - self.num_pages=int(self.num_words/4) - - def run(self): - args = {"seed_num": self.seed_num, "page_index": self.page_index, "num_words": self.num_words, "bip85_index": self.bip85_index} - - NEXT = "Next" - DONE = "Done" - - words_per_page = 4 # TODO: eventually make this configurable for bigger screens? - - mnemonic = self.seed.get_bip85_child_mnemonic(self.bip85_index, self.num_words).split() - words = mnemonic[self.page_index * words_per_page:(self.page_index + 1) * words_per_page] - - button_data = [] - if self.page_index < self.num_pages - 1 or self.seed_num is None: - button_data.append(NEXT) - else: - button_data.append(DONE) - - - button_data = [] - if self.page_index < self.num_pages - 1 or self.seed_num is None: - button_data.append(NEXT) - else: - button_data.append(DONE) - - selected_menu_num = seed_screens.SeedWordsScreen( - title=f"BIP-85: Words: {self.page_index + 1}/{self.num_pages}", - words=words, - page_index=self.page_index, - num_pages=self.num_pages, - button_data=button_data, - - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - if button_data[selected_menu_num] == NEXT: -# if self.seed_num is None and self.page_index == self.num_pages - 1: -# return Destination(SeedFinalizeView) -# else: - return Destination(BIP85SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1, "num_words": self.num_words, "bip85_index": self.bip85_index}) - - elif button_data[selected_menu_num] == DONE: - # Must clear history to avoid BACK button returning to private info - return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) - class SeedBackupView(View): @@ -920,15 +757,20 @@ class SeedExportXpubQRDisplayView(View): View Seed Words flow ****************************************************************************""" class SeedWordsWarningView(View): - def __init__(self, seed_num: int): + def __init__(self, seed_num: int, bip85_data: dict = None): super().__init__() self.seed_num = seed_num + self.bip85_data = bip85_data def run(self): destination = Destination( SeedWordsView, - view_args={"seed_num": self.seed_num, "page_index": 0}, + view_args=dict( + seed_num=self.seed_num, + page_index=0, + bip85_data=self.bip85_data + ), skip_current_view=True, # Prevent going BACK to WarningViews ) if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: @@ -949,13 +791,14 @@ class SeedWordsWarningView(View): class SeedWordsView(View): - def __init__(self, seed_num: int, page_index: int = 0): + def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0): super().__init__() self.seed_num = seed_num if self.seed_num is None: self.seed = self.controller.storage.get_pending_seed() else: self.seed = self.controller.get_seed(self.seed_num) + self.bip85_data = bip85_data self.page_index = page_index self.num_pages=int(len(self.seed.mnemonic_list)/4) @@ -967,7 +810,12 @@ class SeedWordsView(View): # Slice the mnemonic to our current 4-word section words_per_page = 4 # TODO: eventually make this configurable for bigger screens? - mnemonic = self.seed.mnemonic_display_list + if self.bip85_data is not None: + mnemonic = self.seed.get_bip85_child_mnemonic(self.bip85_data["child_index"], self.bip85_data["num_words"]).split() + title = f"""Child #{self.bip85_data["child_index"]}""" + else: + mnemonic = self.seed.mnemonic_display_list + title = "Seed Words" words = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] button_data = [] @@ -976,8 +824,9 @@ class SeedWordsView(View): else: button_data.append(DONE) + selected_menu_num = seed_screens.SeedWordsScreen( - title=f"Seed Words: {self.page_index+1}/{self.num_pages}", + title=f"{title}: {self.page_index+1}/{self.num_pages}", words=words, page_index=self.page_index, num_pages=self.num_pages, @@ -989,13 +838,92 @@ class SeedWordsView(View): if button_data[selected_menu_num] == NEXT: if self.seed_num is None and self.page_index == self.num_pages - 1: - return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestPromptView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) else: - return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1)) + return Destination( + SeedWordsView, + view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1, bip85_data=self.bip85_data) + ) elif button_data[selected_menu_num] == DONE: # Must clear history to avoid BACK button returning to private info - return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestPromptView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) + + + +"""**************************************************************************** + BIP85 - Derive child mnemonic (seed) flow +****************************************************************************""" +class SeedBIP85ApplicationModeView(View): + """ + * Ask the user the application type as defined in the BIP0085 spec. + * Currently only Word mode of 12, 24 words (Application number: 39') + * Possible future additions are + * WIF (HDSEED) + * XPRV (BIP32) + """ + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + self.num_words = 0 + self.bip85_app_num = 39 # TODO: Support other Application numbers + + + def run(self): + # TODO: Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)? + WORDS_12 = "12 Words" + WORDS_24 = "24 Words" + + button_data = [WORDS_12, WORDS_24] + + selected_menu_num = ButtonListScreen( + title="BIP-85 Num Words", + button_data=button_data + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if button_data[selected_menu_num] == WORDS_12: + self.num_words = 12 + elif button_data[selected_menu_num] == WORDS_24: + self.num_words = 24 + + return Destination( + SeedBIP85SelectChildIndexView, + view_args=dict(seed_num=self.seed_num, num_words=self.num_words) + ) + + + +# View to retrieve the derived seed index +class SeedBIP85SelectChildIndexView(View): + def __init__(self, seed_num: int, num_words: int): + super().__init__() + self.seed_num = seed_num + self.num_words = num_words + + + def run(self): + # Change this later to use the generic Screen input keyboard + ret = seed_screens.SeedBIP85SelectChildIndexScreen().display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + return Destination( + SeedWordsWarningView, + view_args=dict( + seed_num=self.seed_num, + bip85_data=dict(child_index=int(ret), num_words=self.num_words), + ) + ) @@ -1003,8 +931,9 @@ class SeedWordsView(View): Seed Words Backup Test ****************************************************************************""" class SeedWordsBackupTestPromptView(View): - def __init__(self, seed_num: int): + def __init__(self, seed_num: int, bip85_data: dict = None): self.seed_num = seed_num + self.bip85_data = bip85_data def run(self): @@ -1016,7 +945,10 @@ class SeedWordsBackupTestPromptView(View): ).display() if button_data[selected_menu_num] == VERIFY: - return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) elif button_data[selected_menu_num] == SKIP: if self.seed_num is not None: @@ -1027,15 +959,20 @@ class SeedWordsBackupTestPromptView(View): class SeedWordsBackupTestView(View): - def __init__(self, seed_num: int, confirmed_list: List[bool] = None, cur_index: int = None): + def __init__(self, seed_num: int, bip85_data: dict = None, confirmed_list: List[bool] = None, cur_index: int = None): super().__init__() self.seed_num = seed_num if self.seed_num is None: self.seed = self.controller.storage.get_pending_seed() else: self.seed = self.controller.get_seed(self.seed_num) + self.bip85_data = bip85_data + + if self.bip85_data is not None: + self.mnemonic_list = self.seed.get_bip85_child_mnemonic(self.bip85_data["child_index"], self.bip85_data["num_words"]).split() + else: + self.mnemonic_list = self.seed.mnemonic_display_list - self.mnemonic_list = self.seed.mnemonic_display_list self.confirmed_list = confirmed_list if not self.confirmed_list: self.confirmed_list = [] @@ -1069,10 +1006,16 @@ class SeedWordsBackupTestView(View): self.confirmed_list.append(self.cur_index) if len(self.confirmed_list) == len(self.mnemonic_list): # Successfully confirmed the full mnemonic! - return Destination(SeedWordsBackupTestSuccessView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsBackupTestSuccessView, + view_args=dict(seed_num=self.seed_num), + ) else: # Continue testing the remaining words - return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list)) + return Destination( + SeedWordsBackupTestView, + view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, bip85_data=self.bip85_data), + ) else: # Picked the WRONG WORD! @@ -1080,6 +1023,7 @@ class SeedWordsBackupTestView(View): SeedWordsBackupTestMistakeView, view_args=dict( seed_num=self.seed_num, + bip85_data=self.bip85_data, cur_index=self.cur_index, wrong_word=button_data[selected_menu_num], confirmed_list=self.confirmed_list, @@ -1089,9 +1033,10 @@ class SeedWordsBackupTestView(View): class SeedWordsBackupTestMistakeView(View): - def __init__(self, seed_num: int, cur_index: int, wrong_word: str, confirmed_list: List[bool] = None): + def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: List[bool] = None): super().__init__() self.seed_num = seed_num + self.bip85_data = bip85_data self.cur_index = cur_index self.wrong_word = wrong_word self.confirmed_list = confirmed_list @@ -1111,10 +1056,21 @@ class SeedWordsBackupTestMistakeView(View): ).display() if button_data[selected_menu_num] == REVIEW: - return Destination(SeedWordsView, view_args=dict(seed_num=self.seed_num)) + return Destination( + SeedWordsView, + view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), + ) elif button_data[selected_menu_num] == RETRY: - return Destination(SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, confirmed_list=self.confirmed_list, cur_index=self.cur_index)) + return Destination( + SeedWordsBackupTestView, + view_args=dict( + seed_num=self.seed_num, + confirmed_list=self.confirmed_list, + cur_index=self.cur_index, + bip85_data=self.bip85_data, + ) + ) From b0b0ca825a2560a66f0c341d67fd3a7dd8c60462 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 11 Nov 2022 13:25:50 -0800 Subject: [PATCH 11/11] applying @newtonick's bugfix on child seed length --- src/seedsigner/views/seed_views.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 57f24980..c935ee7b 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -827,7 +827,6 @@ class SeedWordsView(View): self.seed = self.controller.get_seed(self.seed_num) self.bip85_data = bip85_data self.page_index = page_index - self.num_pages=int(len(self.seed.mnemonic_list)/4) def run(self): @@ -846,17 +845,17 @@ class SeedWordsView(View): words = mnemonic[self.page_index*words_per_page:(self.page_index + 1)*words_per_page] button_data = [] - if self.page_index < self.num_pages - 1 or self.seed_num is None: + num_pages = int(len(mnemonic)/words_per_page) + if self.page_index < num_pages - 1 or self.seed_num is None: button_data.append(NEXT) else: button_data.append(DONE) - selected_menu_num = seed_screens.SeedWordsScreen( - title=f"{title}: {self.page_index+1}/{self.num_pages}", + title=f"{title}: {self.page_index+1}/{num_pages}", words=words, page_index=self.page_index, - num_pages=self.num_pages, + num_pages=num_pages, button_data=button_data, ).display() @@ -864,7 +863,7 @@ class SeedWordsView(View): return Destination(BackStackView) if button_data[selected_menu_num] == NEXT: - if self.seed_num is None and self.page_index == self.num_pages - 1: + if self.seed_num is None and self.page_index == num_pages - 1: return Destination( SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data),