Merge pull request #270 from kdmukai/194_keith_edits

Refactoring/Extending BIP-85 PR
This commit is contained in:
Nick Klockenga
2022-12-05 18:01:04 -05:00
committed by GitHub
6 changed files with 238 additions and 35 deletions
+5 -2
View File
@@ -1034,14 +1034,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:
@@ -1482,3 +1482,17 @@ class MultisigWalletDescriptorScreen(ButtonListScreen):
auto_line_break=True,
allow_text_overflow=True,
))
@dataclass
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 = 5
self.keys_charset = "0123456789"
self.show_save_button = True
super().__post_init__()
+31 -1
View File
@@ -8,6 +8,8 @@ from typing import List
from seedsigner.models.settings import SettingsConstants
from seedsigner.helpers import embit_utils
import hashlib
import hmac
class InvalidSeedException(Exception):
@@ -106,12 +108,40 @@ 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):
return embit_utils.get_xpub(seed_bytes=self.seed_bytes, derivation_path=wallet_path, embit_network=SettingsConstants.map_network_to_embit(network))
# 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):
# 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"])
# 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)
# 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
def __eq__(self, other):
if isinstance(other, Seed):
@@ -155,6 +155,7 @@ class SettingsConstants:
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"
@@ -439,6 +440,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",
+158 -32
View File
@@ -351,6 +351,7 @@ class SeedOptionsView(View):
EXPORT_XPUB = "Export Xpub"
EXPLORER = "Address Explorer"
BACKUP = ("Backup Seed", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)
BIP85_CHILD_SEED = "BIP-85 Child Seed"
DISCARD = ("Discard Seed", None, None, "red")
button_data = []
@@ -374,7 +375,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)
@@ -391,6 +392,10 @@ class SeedOptionsView(View):
button_data.append(EXPLORER)
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(DISCARD)
selected_menu_num = seed_screens.SeedOptionsScreen(
@@ -424,6 +429,9 @@ class SeedOptionsView(View):
elif button_data[selected_menu_num] == BACKUP:
return Destination(SeedBackupView, view_args=dict(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=dict(seed_num=self.seed_num))
@@ -776,15 +784,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:
@@ -805,15 +818,15 @@ 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)
def run(self):
@@ -823,20 +836,26 @@ 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 = []
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"Seed Words: {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()
@@ -844,14 +863,93 @@ 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:
return Destination(SeedWordsBackupTestPromptView, view_args=dict(seed_num=self.seed_num))
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),
)
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),
)
)
@@ -859,9 +957,10 @@ 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):
VERIFY = "Verify"
@@ -872,7 +971,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:
@@ -883,19 +985,24 @@ 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 = []
self.cur_index = cur_index
@@ -904,7 +1011,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)]
@@ -925,17 +1032,24 @@ 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!
return Destination(
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,
@@ -945,14 +1059,15 @@ 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
def run(self):
REVIEW = "Review Seed Words"
RETRY = "Try Again"
@@ -967,17 +1082,28 @@ 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,
)
)
class SeedWordsBackupTestSuccessView(View):
def __init__(self, seed_num: int):
self.seed_num = seed_num
def run(self):
LargeIconStatusScreen(
title="Backup Verified",
@@ -1618,7 +1744,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)
@@ -1637,7 +1763,7 @@ class MultisigWalletDescriptorView(View):
fingerprints.append(fingerprint)
policy = descriptor.brief_policy.split("multisig")[0].strip()
RETURN = "Return to PSBT"
VERIFY = "Verify Addr"
EXPLORER = "Address Explorer"
+23
View File
@@ -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