diff --git a/README.md b/README.md index 318ff4854..59fe0d805 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ If you have specific questions about the project, our [Telegram Group](https://t * Optimized seed word entry interface * Support for Bitcoin Mainnet & Testnet * Support for custom user-defined derivation paths +* Support for loading Electrum Segwit seed phrases with feature limitations: [Electrum support info](docs/electrum.md) * On-demand receive address verification * Address Explorer for single sig and multisig wallets * User-configurable QR code display density diff --git a/docs/electrum.md b/docs/electrum.md new file mode 100644 index 000000000..6c07ca31b --- /dev/null +++ b/docs/electrum.md @@ -0,0 +1,15 @@ +# SeedSigner Electrum seed phrase support + +SeedSigner supports loading of [Electrum's Segwit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). This is considered an Advanced feature that is disabled by default. + +To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After this option is enabled, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen. + +Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic: + +- BIP-85 child seeds + - Not applicable for Electrum seed types +- SeedQR backups + - Since Electrum seeds are not supported by other SeedQR implementations, it would be dangerous to use SeedQR as a backup tool for Electrum seeds and is thus disabled +- Custom derivations + - Hard coded derivation path and script types in SeedSigner to match Electrum wallet software. These are m/0h for single sig and m/1h for multisig + - User-chosen custom derivations are thus not supported for Electrum seeds diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index c368c05e9..269a8f897 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -85,16 +85,15 @@ def is_complete(self): @dataclass class SeedQrEncoder(BaseStaticQrEncoder): - mnemonic: List[str] = None - wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + seed : Seed = None def __post_init__(self): - self.wordlist = Seed.get_wordlist(self.wordlist_language_code) + self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code) super().__post_init__() self.data = "" # Output as Numeric data format - for word in self.mnemonic: + for word in self.seed.mnemonic_list: index = self.wordlist.index(word) self.data += str("%04d" % index) @@ -109,18 +108,18 @@ class CompactSeedQrEncoder(SeedQrEncoder): def next_part(self): # Output as binary data format binary_str = "" - for word in self.mnemonic: + for word in self.seed.mnemonic_list: index = self.wordlist.index(word) # Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits binary_str += bin(index).split('b')[1].zfill(11) # We can exclude the checksum bits at the end - if len(self.mnemonic) == 24: + if len(self.seed.mnemonic_list) == 24: # 8 checksum bits in a 24-word seed binary_str = binary_str[:-8] - elif len(self.mnemonic) == 12: + elif len(self.seed.mnemonic_list) == 12: # 4 checksum bits in a 12-word seed binary_str = binary_str[:-4] @@ -149,22 +148,14 @@ class BaseXpubQrEncoder(BaseQrEncoder): """ Base Xpub QrEncoder for static and animated formats """ - mnemonic: list = None - passphrase: str = None + seed: Seed = None derivation: str = None network: str = SettingsConstants.MAINNET - wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH + sig_type : str = None def prep_xpub(self): - self.wordlist = Seed.get_wordlist(self.wordlist_language_code) - - if self.wordlist == None: - raise Exception('Wordlist Required') - version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]) - self.seed = Seed(mnemonic=self.mnemonic, - passphrase=self.passphrase, - wordlist_language_code=self.wordlist_language_code) + version = self.seed.detect_version(self.derivation, self.network, self.sig_type) self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(self.network)]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 51eb85553..6164ff84c 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -1,4 +1,6 @@ import unicodedata +import hashlib +import hmac from binascii import hexlify from embit import bip39, bip32, bip85 @@ -18,7 +20,7 @@ def __init__(self, mnemonic: List[str] = None, passphrase: str = "", wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> None: - self.wordlist_language_code = wordlist_language_code + self._wordlist_language_code = wordlist_language_code if not mnemonic: raise Exception("Must initialize a Seed with a mnemonic List[str]") @@ -56,7 +58,11 @@ def mnemonic_str(self) -> str: @property def mnemonic_list(self) -> List[str]: return self._mnemonic - + + + @property + def wordlist_language_code(self) -> str: + return self._wordlist_language_code @property def mnemonic_display_str(self) -> str: @@ -100,6 +106,28 @@ def set_wordlist_language_code(self, language_code: str): # TODO: Support other BIP-39 wordlist languages! raise Exception("Not yet implemented!") + @property + def script_override(self) -> list: + return None + + def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + return None + + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] + return bip32.detect_version(derivation_path, default="xpub", network=embit_network) + + @property + def passphrase_label(self) -> str: + return SettingsConstants.LABEL__BIP39_PASSPHRASE + + @property + def seedqr_supported(self) -> bool: + return True + + @property + def bip85_supported(self) -> bool: + return True 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"]) @@ -125,3 +153,65 @@ def __eq__(self, other): if isinstance(other, Seed): return self.seed_bytes == other.seed_bytes return False + + + +class ElectrumSeed(Seed): + + + def _generate_seed(self) -> bool: + if len(self._mnemonic) != 12: + return False + s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex() + prefix = s[0:3] + # only support Electrum Segwit version for now + if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix: + self.seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS) + return True + else: + raise InvalidSeedException("Unsupported electrum seed input") + return False + + def set_passphrase(self, passphrase: str, regenerate_seed: bool = True): + if passphrase: + self._passphrase = ElectrumSeed.normalize_electrum_passphrase(passphrase) + else: + # Passphrase must always have a string value, even if it's just the empty + # string. + self._passphrase = "" + + if regenerate_seed: + # Regenerate the internal seed since passphrase changes the result + self._generate_seed() + + @staticmethod + def normalize_electrum_passphrase(passphrase : str) -> str: + passphrase = unicodedata.normalize('NFKD', passphrase) + # lower + passphrase = passphrase.lower() + # normalize whitespaces + passphrase = u' '.join(passphrase.split()) + return passphrase + + @property + def script_override(self) -> list: + return [SettingsConstants.NATIVE_SEGWIT] + + def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h" + + def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str: + embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)] + return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"] + + @property + def passphrase_label(self) -> str: + return SettingsConstants.LABEL__CUSTOM_EXTENSION + + @property + def seedqr_supported(self) -> bool: + return False + + @property + def bip85_supported(self) -> bool: + return False diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index 5b6c30495..85feb49cb 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -1,5 +1,5 @@ from typing import List -from seedsigner.models.seed import Seed, InvalidSeedException +from seedsigner.models.seed import Seed, ElectrumSeed, InvalidSeedException from seedsigner.models.settings_definition import SettingsConstants @@ -9,6 +9,7 @@ def __init__(self) -> None: self.seeds: List[Seed] = [] self.pending_seed: Seed = None self._pending_mnemonic: List[str] = [] + self._pending_is_electrum : bool = False def set_pending_seed(self, seed: Seed): @@ -58,8 +59,9 @@ def pending_mnemonic_length(self) -> int: return len(self._pending_mnemonic) - def init_pending_mnemonic(self, num_words:int = 12): + def init_pending_mnemonic(self, num_words:int = 12, is_electrum:bool = False): self._pending_mnemonic = [None] * num_words + self._pending_is_electrum = is_electrum def update_pending_mnemonic(self, word: str, index: int): @@ -81,16 +83,23 @@ def get_pending_mnemonic_word(self, index: int) -> str: def get_pending_mnemonic_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str: try: - seed = Seed(self._pending_mnemonic) + if self._pending_is_electrum: + seed = ElectrumSeed(self._pending_mnemonic) + else: + seed = Seed(self._pending_mnemonic) return seed.get_fingerprint(network) except InvalidSeedException: return None def convert_pending_mnemonic_to_pending_seed(self): - self.pending_seed = Seed(self._pending_mnemonic) + if self._pending_is_electrum: + self.pending_seed = ElectrumSeed(self._pending_mnemonic) + else: + self.pending_seed = Seed(self._pending_mnemonic) self.discard_pending_mnemonic() def discard_pending_mnemonic(self): self._pending_mnemonic = [] + self._pending_is_electrum = False diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index c329d82e3..8f33ee6b4 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -160,6 +160,7 @@ def map_network_to_embit(cls, network) -> str: SETTING__CAMERA_ROTATION = "camera_rotation" SETTING__COMPACT_SEEDQR = "compact_seedqr" SETTING__BIP85_CHILD_SEEDS = "bip85_child_seeds" + SETTING__ELECTRUM_SEEDS = "electrum_seeds" SETTING__MESSAGE_SIGNING = "message_signing" SETTING__PRIVACY_WARNINGS = "privacy_warnings" SETTING__DIRE_WARNINGS = "dire_warnings" @@ -198,6 +199,15 @@ def map_network_to_embit(cls, network) -> str: TYPE__ENABLED_DISABLED_PROMPT_REQUIRED, ] + # Electrum seed constants + ELECTRUM_SEED_STANDARD = "01" + ELECTRUM_SEED_SEGWIT = "100" + ELECTRUM_SEED_2FA = "101" + ELECTRUM_PBKDF2_ROUNDS=2048 + + # Label strings + LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase" + LABEL__CUSTOM_EXTENSION = "Custom Extension" @dataclass class SettingsEntry: @@ -458,6 +468,13 @@ class SettingsDefinition: visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, + attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, + abbreviated_name="Electrum", + display_name="Electrum seed support (Native Segwit only)", + visibility=SettingsConstants.VISIBILITY__ADVANCED, + default_value=SettingsConstants.OPTION__DISABLED), + SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES, attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING, display_name="Message signing", diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 18e403127..98ad5c204 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -17,6 +17,7 @@ class PSBTSelectSeedView(View): SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) def run(self): @@ -44,6 +45,8 @@ def run(self): button_data.append(self.SCAN_SEED) button_data.append(self.TYPE_12WORD) button_data.append(self.TYPE_24WORD) + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.append(self.TYPE_ELECTRUM) selected_menu_num = self.run_screen( ButtonListScreen, @@ -75,6 +78,18 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + self.run_screen( + WarningScreen, + title="Electrum warning", + status_headline=None, + text=f"Some features disabled for Electrum seeds", + show_back_button=False, + ) + from seedsigner.views.seed_views import SeedMnemonicEntryView + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + class PSBTOverviewView(View): diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 9ec874dad..215af14f5 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -79,6 +79,7 @@ class SeedSelectSeedView(View): SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) def __init__(self, flow: str = Controller.FLOW__VERIFY_SINGLESIG_ADDR): @@ -118,6 +119,8 @@ def run(self): button_data.append(self.SCAN_SEED) button_data.append(self.TYPE_12WORD) button_data.append(self.TYPE_24WORD) + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.append(self.TYPE_ELECTRUM) selected_menu_num = self.run_screen( seed_screens.SeedSelectSeedScreen, @@ -154,6 +157,19 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + self.run_screen( + WarningScreen, + title="Electrum warning", + status_headline=None, + text=f"Some features disabled for Electrum seeds", + show_back_button=False, + ) + + from seedsigner.views.seed_views import SeedMnemonicEntryView + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + """**************************************************************************** @@ -163,6 +179,7 @@ class LoadSeedView(View): SEED_QR = (" Scan a SeedQR", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) CREATE = (" Create a seed", SeedSignerIconConstants.PLUS) def run(self): @@ -172,6 +189,9 @@ def run(self): self.TYPE_24WORD, self.CREATE, ] + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.insert(len(button_data)-1, self.TYPE_ELECTRUM) + selected_menu_num = self.run_screen( ButtonListScreen, title="Load A Seed", @@ -194,6 +214,17 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + self.run_screen( + WarningScreen, + title="Electrum warning", + status_headline=None, + text=f"Some features disabled for Electrum seeds", + show_back_button=False, + ) + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.CREATE: from .tools_views import ToolsMenuView return Destination(ToolsMenuView) @@ -288,7 +319,6 @@ def run(self): class SeedFinalizeView(View): FINALIZE = "Done" - PASSPHRASE = "BIP-39 Passphrase" def __init__(self): super().__init__() @@ -298,8 +328,9 @@ def __init__(self): def run(self): button_data = [self.FINALIZE] + passphrase_button = self.seed.passphrase_label if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: - button_data.append(self.PASSPHRASE) + button_data.append(passphrase_button) selected_menu_num = self.run_screen( seed_screens.SeedFinalizeScreen, @@ -311,7 +342,7 @@ def run(self): seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) - elif button_data[selected_menu_num] == self.PASSPHRASE: + elif button_data[selected_menu_num] == passphrase_button: return Destination(SeedAddPassphraseView) @@ -323,7 +354,8 @@ def __init__(self): def run(self): - ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase) + passphrase_title=self.seed.passphrase_label + ret = self.run_screen(seed_screens.SeedAddPassphraseScreen, passphrase=self.seed.passphrase, title=passphrase_title) if ret == RET_CODE__BACK_BUTTON: return Destination(BackStackView) @@ -487,7 +519,7 @@ def run(self): if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED: button_data.append(self.SIGN_MESSAGE) - if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED: + if self.settings.get_value(SettingsConstants.SETTING__BIP85_CHILD_SEEDS) == SettingsConstants.OPTION__ENABLED and self.seed.bip85_supported: button_data.append(self.BIP85_CHILD_SEED) button_data.append(self.DISCARD) @@ -546,7 +578,9 @@ def __init__(self, seed_num): def run(self): - button_data = [self.VIEW_WORDS, self.EXPORT_SEEDQR] + button_data = [self.VIEW_WORDS] + if self.seed.seedqr_supported: + button_data.append(self.EXPORT_SEEDQR) selected_menu_num = self.run_screen( ButtonListScreen, @@ -612,9 +646,11 @@ def __init__(self, seed_num: int, sig_type: str): def run(self): from .tools_views import ToolsAddressExplorerAddressTypeView args = {"seed_num": self.seed_num, "sig_type": self.sig_type} - if len(self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)) == 1: + seed = self.controller.storage.seeds[self.seed_num] + script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES) + if len(script_types) == 1: # Nothing to select; skip this screen - args["script_type"] = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)[0] + args["script_type"] = script_types[0] if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: del args["sig_type"] @@ -798,8 +834,11 @@ def __init__(self, seed_num: int, sig_type: str, script_type: str, coordinator: def run(self): + seed_derivation_override = self.seed.derivation_override(self.sig_type) if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation + elif seed_derivation_override: + derivation_path = seed_derivation_override else: derivation_path = embit_utils.get_standard_derivation_path( network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), @@ -819,10 +858,10 @@ def run(self): try: embit_network = NETWORKS[SettingsConstants.map_network_to_embit(self.settings.get_value(SettingsConstants.SETTING__NETWORK))] - version = embit.bip32.detect_version( + version = self.seed.detect_version( derivation_path, - default="xpub", - network=embit_network + self.settings.get_value(SettingsConstants.SETTING__NETWORK), + self.sig_type ) root = embit.bip32.HDKey.from_seed( self.seed.seed_bytes, @@ -850,6 +889,7 @@ def run(self): dict(seed_num=self.seed_num, coordinator=self.coordinator, derivation_path=derivation_path, + sig_type=self.sig_type ) ) @@ -859,16 +899,16 @@ def run(self): class SeedExportXpubQRDisplayView(View): - def __init__(self, seed_num: int, coordinator: str, derivation_path: str): + def __init__(self, seed_num: int, coordinator: str, derivation_path: str, sig_type: str = SettingsConstants.SINGLE_SIG): super().__init__() self.seed = self.controller.get_seed(seed_num) encoder_args = dict( - mnemonic=self.seed.mnemonic_list, - passphrase=self.seed.passphrase, + seed=self.seed, derivation=derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), - qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY) + qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY), + sig_type=sig_type ) if coordinator == SettingsConstants.COORDINATOR__SPECTER_DESKTOP: @@ -1381,8 +1421,7 @@ def __init__(self, seed_num: int, seedqr_format: str, num_modules: int): def run(self): - encoder_args = dict(mnemonic=self.seed.mnemonic_list, - wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + encoder_args = dict(seed=self.seed) if self.seedqr_format == QRType.SEED__SEEDQR: e = SeedQrEncoder(**encoder_args) elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR: @@ -1418,8 +1457,7 @@ def __init__(self, seed_num: int, seedqr_format: str): def run(self): - encoder_args = dict(mnemonic=self.seed.mnemonic_list, - wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + encoder_args = dict(seed=self.seed) if self.seedqr_format == QRType.SEED__SEEDQR: e = SeedQrEncoder(**encoder_args) elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR: @@ -1641,15 +1679,17 @@ def __init__(self, seed_num: int = None): super().__init__() self.seed_num = seed_num self.is_multisig = self.controller.unverified_address["sig_type"] == SettingsConstants.MULTISIG + self.seed_derivation_override = "" if not self.is_multisig: if seed_num is None: raise Exception("Can't validate a single sig addr without specifying a seed") self.seed_num = seed_num self.seed = self.controller.get_seed(seed_num) + self.seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG) else: self.seed = None self.address = self.controller.unverified_address["address"] - self.derivation_path = self.controller.unverified_address["derivation_path"] + self.derivation_path = self.seed_derivation_override if self.seed_derivation_override else self.controller.unverified_address["derivation_path"] self.script_type = self.controller.unverified_address["script_type"] self.sig_type = self.controller.unverified_address["sig_type"] self.network = self.controller.unverified_address["network"] diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 02c34f230..ad2940bea 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -9,7 +9,7 @@ from seedsigner.controller import Controller from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants -from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen) +from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen) from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen, ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen, ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen) @@ -440,6 +440,7 @@ class ToolsAddressExplorerSelectSourceView(View): SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) def run(self): @@ -449,6 +450,8 @@ def run(self): button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT)) button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD] + if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED: + button_data.append(self.TYPE_ELECTRUM) selected_menu_num = self.run_screen( ButtonListScreen, @@ -492,6 +495,18 @@ def run(self): self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) + elif button_data[selected_menu_num] == self.TYPE_ELECTRUM: + self.run_screen( + WarningScreen, + title="Electrum warning", + status_headline=None, + text=f"Some features disabled for Electrum seeds", + show_back_button=False, + ) + from seedsigner.views.seed_views import SeedMnemonicEntryView + self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True) + return Destination(SeedMnemonicEntryView) + class ToolsAddressExplorerAddressTypeView(View): @@ -525,9 +540,12 @@ def __init__(self, seed_num: int = None, script_type: str = None, custom_derivat if self.seed_num is not None: self.seed = self.controller.storage.seeds[seed_num] data["seed_num"] = self.seed + seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG) if self.script_type == SettingsConstants.CUSTOM_DERIVATION: derivation_path = self.custom_derivation + elif seed_derivation_override: + derivation_path = seed_derivation_override else: derivation_path = embit_utils.get_standard_derivation_path( network=self.settings.get_value(SettingsConstants.SETTING__NETWORK), diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 8a8ec3688..34f286319 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -3,6 +3,7 @@ from binascii import a2b_base64 from seedsigner.models.settings import SettingsConstants +from seedsigner.models.seed import Seed @@ -24,18 +25,19 @@ def test_ur_psbt_qr_encode(): def test_seedsigner_qr(): # test vector 1 from the SeedQR docs mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() - e = SeedQrEncoder(mnemonic=mnemonic) + e = SeedQrEncoder(seed=Seed(mnemonic)) assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643" - e = CompactSeedQrEncoder(mnemonic=mnemonic) + e = CompactSeedQrEncoder(seed=Seed(mnemonic)) assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n' # test vector 4 from the SeedQR docs mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split() - e = SeedQrEncoder(mnemonic=mnemonic) + seed = Seed(mnemonic) + e = SeedQrEncoder(seed=seed) assert e.next_part() == "073318950739065415961602009907670428187212261116" - e = CompactSeedQrEncoder(mnemonic=mnemonic) + e = CompactSeedQrEncoder(seed=seed) assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE' @@ -43,8 +45,7 @@ def test_seedsigner_qr(): def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = StaticXpubQrEncoder(mnemonic=mnemonic.split(), - passphrase="pass", + e = StaticXpubQrEncoder(seed=Seed(mnemonic.split(), passphrase="pass"), derivation="m/48h/1h/0h/2h", network=SettingsConstants.TESTNET) assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" @@ -54,7 +55,7 @@ def test_xpub_qr(): def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = SpecterXPubQrEncoder(mnemonic=mnemonic.split(" "), passphrase="pass", network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) + e = SpecterXPubQrEncoder(seed=Seed(mnemonic.split(" "), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__LOW) assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" @@ -67,8 +68,7 @@ def test_ur_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" e = UrXpubQrEncoder( - mnemonic=mnemonic.split(), - passphrase="pass", + seed=Seed(mnemonic.split(), passphrase="pass"), network=SettingsConstants.TESTNET, derivation="m/48h/1h/0h/2h", qr_density=SettingsConstants.DENSITY__MEDIUM diff --git a/tests/test_flows_psbt.py b/tests/test_flows_psbt.py index 3f4873808..830f0cf51 100644 --- a/tests/test_flows_psbt.py +++ b/tests/test_flows_psbt.py @@ -2,6 +2,7 @@ from seedsigner.views.view import MainMenuView from seedsigner.views import scan_views, seed_views, psbt_views +from seedsigner.models.settings import SettingsConstants class TestPSBTFlows(FlowTest): @@ -77,7 +78,7 @@ def load_seed_into_decoder(view: scan_views.ScanView): FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED), FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED), FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder), - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="abc"), FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), FlowStep(seed_views.SeedOptionsView, is_redirect=True), diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index e71ad0b54..84b18eb81 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -40,7 +40,7 @@ def test_passphrase_entry_flow(self): self.run_sequence([ FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN), FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="muhpassphrase"), FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.EDIT), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="muhpassphrase2"), diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py index f24f5a230..555142ba5 100644 --- a/tests/test_flows_tools.py +++ b/tests/test_flows_tools.py @@ -64,7 +64,7 @@ def load_seed_into_decoder(view: scan_views.ScanView): # Finalize the new seed w/passphrase self.run_sequence( sequence=[ - FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE), + FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE), FlowStep(seed_views.SeedAddPassphraseView, screen_return_value="mypassphrase"), FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE), FlowStep(seed_views.SeedOptionsView, is_redirect=True), diff --git a/tests/test_seed.py b/tests/test_seed.py index a7e082eee..04291f65f 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -1,4 +1,4 @@ -from seedsigner.models.seed import Seed +from seedsigner.models.seed import Seed, ElectrumSeed from seedsigner.models.settings import SettingsConstants @@ -36,6 +36,10 @@ def test_seed(): # assert seed.passphrase == "test" - - - \ No newline at end of file +def test_electrum_seed(): + seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split()) + + intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA' + + assert seed.seed_bytes == intended_seed + diff --git a/tests/test_seedqr.py b/tests/test_seedqr.py index 826cd9415..2202af0df 100644 --- a/tests/test_seedqr.py +++ b/tests/test_seedqr.py @@ -4,6 +4,7 @@ from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder from seedsigner.models.qr_type import QRType +from seedsigner.models.seed import Seed @@ -13,9 +14,9 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type): assert len(mnemonic) == mnemonic_length if qr_type == QRType.SEED__SEEDQR: - e = SeedQrEncoder(mnemonic=mnemonic) + e = SeedQrEncoder(seed=Seed(mnemonic)) elif qr_type == QRType.SEED__COMPACTSEEDQR: - e = CompactSeedQrEncoder(mnemonic=mnemonic) + e = CompactSeedQrEncoder(seed=Seed(mnemonic)) data = e.next_part() print(data)