Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for Electrum Standard (P2PKH) seed phrases #581

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +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)
* Support for loading Electrum 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
Expand Down
6 changes: 4 additions & 2 deletions docs/electrum.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# 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.
SeedSigner supports loading of [Electrum's Standard(P2PKH) and Native Segwit seed phrases](https://electrum.readthedocs.io/en/latest/seedphrase.html#electrum-seed-version-system). The "old" mnemonics used prior to Electrum wallet software version 2.0 are not currently supported.

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.
To load an Electrum Segwit seed phrase, first enable Electrum seed support in Settings -> Advanced -> Electrum seed support. After an electrum seed length is selected, the user will now be able to enter an Electrum seed phrase by selecting "Enter Electrum seed" in the Load Seed screen.

The user has the option to load a 12 word or 13 word seed phrase. Note that the 13th word does NOT refer to the custom extension words (passphrase), but rather Electrum wallet software used 13 word seed phrases from version 2.0 up until prior to version 2.7. If you need to enter the words you used for "Extend this seed with custom words" in the electrum software, first enter the seed, then before finalizing you will be given the option to add a custom extension similar to the workflow for entering a BIP-39 seed and BIP-39 passphrase.

Some SeedSigner functionality is deliberately disabled when using an Electrum mnemonic:

Expand Down
23 changes: 17 additions & 6 deletions src/seedsigner/models/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,26 @@ def __eq__(self, other):
class ElectrumSeed(Seed):

def _generate_seed(self):
if len(self._mnemonic) != 12:
raise InvalidSeedException(f"Unsupported Electrum seed length: {len(self._mnemonic)}")

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)
self._singlesig_derivation = "m/0h"
self._multisig_derivation = "m/1h"
self._script_override = SettingsConstants.NATIVE_SEGWIT
self._electrum_seed_type = SettingsConstants.ELECTRUM_SEED_SEGWIT

elif prefix.startswith(SettingsConstants.ELECTRUM_SEED_STANDARD):
self._singlesig_derivation = "m"
self._multisig_derivation = "m"
self._script_override = SettingsConstants.LEGACY_P2PKH
self._electrum_seed_type = SettingsConstants.ELECTRUM_SEED_STANDARD

else:
raise InvalidSeedException(f"Unsupported Electrum seed format: {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)


def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
Expand Down Expand Up @@ -209,16 +217,19 @@ def normalize_electrum_passphrase(passphrase : str) -> str:

@property
def script_override(self) -> str:
return SettingsConstants.NATIVE_SEGWIT
return self._script_override


def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
return "m/0h" if sig_type == SettingsConstants.SINGLE_SIG else "m/1h"
return self._singlesig_derivation if SettingsConstants.SINGLE_SIG == sig_type else self._multisig_derivation


def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
return embit_network["zpub"] if sig_type == SettingsConstants.SINGLE_SIG else embit_network["Zpub"]
if SettingsConstants.ELECTRUM_SEED_SEGWIT == self._electrum_seed_type:
return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == sig_type else embit_network["Zpub"]
else:
return embit_network["xpub"]


@property
Expand Down
14 changes: 12 additions & 2 deletions src/seedsigner/models/settings_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ def map_network_to_embit(cls, network) -> str:
ELECTRUM_SEED_SEGWIT = "100"
ELECTRUM_SEED_2FA = "101"
ELECTRUM_PBKDF2_ROUNDS=2048
ELECTRUM_SEED_NONE="electrum_none"
ELECTRUM_SEED_12WORD="electrum_12w"
ELECTRUM_SEED_13WORD="electrum_13w"
ALL_ELECTRUM_SEEDS = [
(ELECTRUM_SEED_NONE, "Disabled"),
(ELECTRUM_SEED_12WORD, "12 Word Seeds"),
(ELECTRUM_SEED_13WORD, "13 Word Seeds"),
]

# Label strings
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
Expand Down Expand Up @@ -475,9 +483,11 @@ class SettingsDefinition:
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
abbreviated_name="electrum",
display_name="Electrum seeds",
help_text="Native Segwit only",
help_text="Most Electrum seeds are 12 words, see docs if unsure",
type=SettingsConstants.TYPE__SELECT_1,
selection_options=SettingsConstants.ALL_ELECTRUM_SEEDS,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
default_value=SettingsConstants.ELECTRUM_SEED_NONE),

SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING,
Expand Down
13 changes: 9 additions & 4 deletions src/seedsigner/views/seed_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def run(self):
button_data.append(self.TYPE_12WORD)
button_data.append(self.TYPE_24WORD)

if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
if SettingsConstants.ELECTRUM_SEED_NONE != self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS):
button_data.append(self.TYPE_ELECTRUM)

selected_menu_num = self.run_screen(
Expand Down Expand Up @@ -181,7 +181,7 @@ def run(self):
self.TYPE_24WORD,
]

if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
if SettingsConstants.ELECTRUM_SEED_NONE != self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS):
button_data.append(self.TYPE_ELECTRUM)

button_data.append(self.CREATE)
Expand Down Expand Up @@ -490,9 +490,14 @@ def run(self):
show_back_button=False,
)

self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
enabled_electrum_length = self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS)
if SettingsConstants.ELECTRUM_SEED_13WORD == enabled_electrum_length:
self.controller.storage.init_pending_mnemonic(num_words=13, is_electrum=True)
else:
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)

return Destination(SeedMnemonicEntryView, skip_current_view=True)

return Destination(SeedMnemonicEntryView)



Expand Down
2 changes: 1 addition & 1 deletion src/seedsigner/views/tools_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ 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:
if SettingsConstants.ELECTRUM_SEED_NONE != self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS):
button_data.append(self.TYPE_ELECTRUM)

selected_menu_num = self.run_screen(
Expand Down
8 changes: 7 additions & 1 deletion tests/test_flows_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ def test_electrum_mnemonic_entry_flow(self):
def test_with_mnemonic(mnemonic: list[str], custom_extension: str = None, expects_electrum_seed_is_valid: bool = True):
Settings.HOSTNAME = "not seedsigner-os"
settings = Settings.get_instance()
settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)

length_choice = SettingsConstants.ELECTRUM_SEED_13WORD if 13==len(mnemonic) else SettingsConstants.ELECTRUM_SEED_12WORD
settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, length_choice)


sequence = [
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
Expand Down Expand Up @@ -175,6 +178,9 @@ def test_with_mnemonic(mnemonic: list[str], custom_extension: str = None, expect
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="test")
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="monkey fling orange coin good")

# Test 13 word seed generated by Electrum v2
test_with_mnemonic("usual burger donate pig aim cycle expose economy pause hint argue dish ability".split())

# Most BIP-39 seeds should fail; test seed generated by bitcoiner.guide
test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False)

Expand Down
17 changes: 7 additions & 10 deletions tests/test_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ def test_seed():
# assert seed.passphrase == "test"


def test_electrum_seed():
def test_electrum_segwit_seed():
"""
ElectrumSeed should correctly parse a modern Electrum mnemonic.
ElectrumSeed should correctly parse a Native Segwit Electrum mnemonic.
"""
seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split())

Expand All @@ -49,18 +49,15 @@ def test_electrum_seed():
assert seed.seed_bytes == intended_seed


def test_electrum_mnemonic_format():
def test_electrum_standard_seed():
"""
ElectrumSeed should reject mnemonics that are not 12 words long.
ElectrumSeed should correctly parse a Standard Electrum mnemonic.
"""
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic=["regular"] * 11)
seed = ElectrumSeed(mnemonic="valve attack fence zero swim frequent visa myth tobacco dismiss useless marble".split())

with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic=["regular"] * 13)
intended_seed = b'\x0c$\x97\xb1r\x11{\xdf\xa8\xe6\xb8\xa7!_\xf6\xb9\xacz\x08\xbe5Fa\xeb\xd6\xb7.#\xb6:=\xf7_hZY\xc2\x9b:W\xdc!f\x16\x7f\x98\x99k\x90\x8f1t>Qq\xeb\xf3\x96@\x91}\x19\x1cy'

with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic=["regular"] * 24)
assert seed.seed_bytes == intended_seed


def test_electrum_seed_rejects_most_bip39_mnemonics():
Expand Down
Loading