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

Add limited support for electrum segwit seeds #513

Merged
merged 22 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c1d320f
Add limited support for electrum segwit seeds
Dec 4, 2023
89f6cec
making suggested changes for electrum derivation path and removing on…
Dec 18, 2023
eaa29ab
derivation path override to make address verifiction work for electru…
Dec 18, 2023
1f0a4dd
since EncodeQR generates the seed internally for xpub export, we have…
Dec 18, 2023
289b382
pass seed to EncodeQR instead of mnemonic and passphrase to simplify
Dec 19, 2023
656110b
Revert "pass seed to EncodeQR instead of mnemonic and passphrase to s…
Dec 19, 2023
f8aca10
export correct xpub version bytes for electrum seeds
Dec 24, 2023
8f0a8fe
Merge branch 'SeedSigner:dev' into PRChanges
BamaHodl Mar 5, 2024
40520f8
integrating PR review recommendations for electrum seed support
Mar 19, 2024
8c9fd5c
display warning screen before user inputs electrum seed to notify som…
Mar 19, 2024
1820997
disabled BIP85 child seeds for electrum seeds since it doesn't apply
Mar 19, 2024
2355c53
updating docs to elaborate on current Electrum seed support and limit…
Mar 19, 2024
ce9113f
fix bad paste in unverified address deriv path
Mar 19, 2024
0fc3ce4
merging main upstream repo into fork
Jun 21, 2024
8a83f32
remove unneccessary include
Jun 21, 2024
c0f1ad6
small cleanup
Jun 21, 2024
b430d50
pass sig_type to BaseXpubQrEncoder so that it can get correct version
Jun 21, 2024
d483864
fixing merge conflict with new test in test_flows_tools.py
Jun 30, 2024
a28f162
Update electrum.md
BamaHodl Jul 8, 2024
4ea2d72
more explicit about which electrum seeds supported in Advanced option…
Jul 8, 2024
46061c5
correcting electrum doc to match change in option name
Jul 8, 2024
75d9f5f
make sure to use the correct list for script_types
Jul 8, 2024
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions docs/electrum.md
Original file line number Diff line number Diff line change
@@ -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
Copy link

@jdlcdl jdlcdl Jun 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand not doing this for bip85 because the user might NOT be expecting a bip39 mnemonic. But wondering why no custom derivation for electrum seeds??? Is it about electrum not using standard derivation paths?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Electrum doesn't use standard derivation paths, or let users edit them, they are basically hardcoded based on the seed type.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to explicitly add the hard coded derivations in SeedSigner in this document.

Suggestion:

  • Custom Derivations
    • Hard coded derivation path and script types in SeedSigner to match Electrum. These are m/0h for single sig and m/1h for multisig to match Electrum

- 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
27 changes: 9 additions & 18 deletions src/seedsigner/models/encode_qr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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]

Expand Down Expand Up @@ -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)
Expand Down
94 changes: 92 additions & 2 deletions src/seedsigner/models/seed.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import unicodedata
import hashlib
import hmac

from binascii import hexlify
from embit import bip39, bip32, bip85
Expand All @@ -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]")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"])
Expand All @@ -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
17 changes: 13 additions & 4 deletions src/seedsigner/models/seed_storage.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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
17 changes: 17 additions & 0 deletions src/seedsigner/models/settings_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions src/seedsigner/views/psbt_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
Loading
Loading