From 1330d67627bcc5eb5467c8f4b5f0e51fb4d5a1cd Mon Sep 17 00:00:00 2001 From: jase100k Date: Wed, 27 Apr 2022 20:04:51 +1000 Subject: [PATCH] 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