mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
More tests; minor reformatting
This commit is contained in:
@@ -42,7 +42,7 @@ class Seed:
|
|||||||
raise Exception(f"Unrecognized wordlist_language_code {wordlist_language_code}")
|
raise Exception(f"Unrecognized wordlist_language_code {wordlist_language_code}")
|
||||||
|
|
||||||
|
|
||||||
def _generate_seed(self) -> bool:
|
def _generate_seed(self):
|
||||||
try:
|
try:
|
||||||
self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist)
|
self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -64,6 +64,7 @@ class Seed:
|
|||||||
def wordlist_language_code(self) -> str:
|
def wordlist_language_code(self) -> str:
|
||||||
return self._wordlist_language_code
|
return self._wordlist_language_code
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mnemonic_display_str(self) -> str:
|
def mnemonic_display_str(self) -> str:
|
||||||
return unicodedata.normalize("NFC", " ".join(self._mnemonic))
|
return unicodedata.normalize("NFC", " ".join(self._mnemonic))
|
||||||
@@ -106,29 +107,36 @@ class Seed:
|
|||||||
# TODO: Support other BIP-39 wordlist languages!
|
# TODO: Support other BIP-39 wordlist languages!
|
||||||
raise Exception("Not yet implemented!")
|
raise Exception("Not yet implemented!")
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def script_override(self) -> list:
|
def script_override(self) -> list:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
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)]
|
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
|
||||||
return bip32.detect_version(derivation_path, default="xpub", network=embit_network)
|
return bip32.detect_version(derivation_path, default="xpub", network=embit_network)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def passphrase_label(self) -> str:
|
def passphrase_label(self) -> str:
|
||||||
return SettingsConstants.LABEL__BIP39_PASSPHRASE
|
return SettingsConstants.LABEL__BIP39_PASSPHRASE
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def seedqr_supported(self) -> bool:
|
def seedqr_supported(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bip85_supported(self) -> bool:
|
def bip85_supported(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str:
|
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"])
|
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')
|
return hexlify(root.child(0).fingerprint).decode('utf-8')
|
||||||
@@ -158,19 +166,20 @@ class Seed:
|
|||||||
|
|
||||||
class ElectrumSeed(Seed):
|
class ElectrumSeed(Seed):
|
||||||
|
|
||||||
|
def _generate_seed(self):
|
||||||
def _generate_seed(self) -> bool:
|
|
||||||
if len(self._mnemonic) != 12:
|
if len(self._mnemonic) != 12:
|
||||||
return False
|
raise InvalidSeedException(f"Unsupported Electrum seed length: {len(self._mnemonic)}")
|
||||||
|
|
||||||
s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex()
|
s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex()
|
||||||
prefix = s[0:3]
|
prefix = s[0:3]
|
||||||
|
|
||||||
# only support Electrum Segwit version for now
|
# only support Electrum Segwit version for now
|
||||||
if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix:
|
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.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:
|
else:
|
||||||
raise InvalidSeedException("Unsupported electrum seed input")
|
raise InvalidSeedException(f"Unsupported Electrum seed format: {prefix}")
|
||||||
return False
|
|
||||||
|
|
||||||
def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
|
def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
|
||||||
if passphrase:
|
if passphrase:
|
||||||
@@ -184,6 +193,7 @@ class ElectrumSeed(Seed):
|
|||||||
# Regenerate the internal seed since passphrase changes the result
|
# Regenerate the internal seed since passphrase changes the result
|
||||||
self._generate_seed()
|
self._generate_seed()
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize_electrum_passphrase(passphrase : str) -> str:
|
def normalize_electrum_passphrase(passphrase : str) -> str:
|
||||||
passphrase = unicodedata.normalize('NFKD', passphrase)
|
passphrase = unicodedata.normalize('NFKD', passphrase)
|
||||||
@@ -193,25 +203,31 @@ class ElectrumSeed(Seed):
|
|||||||
passphrase = u' '.join(passphrase.split())
|
passphrase = u' '.join(passphrase.split())
|
||||||
return passphrase
|
return passphrase
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def script_override(self) -> list:
|
def script_override(self) -> list:
|
||||||
return [SettingsConstants.NATIVE_SEGWIT]
|
return [SettingsConstants.NATIVE_SEGWIT]
|
||||||
|
|
||||||
|
|
||||||
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
|
||||||
return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h"
|
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:
|
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)]
|
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
|
||||||
return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"]
|
return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"]
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def passphrase_label(self) -> str:
|
def passphrase_label(self) -> str:
|
||||||
return SettingsConstants.LABEL__CUSTOM_EXTENSION
|
return SettingsConstants.LABEL__CUSTOM_EXTENSION
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def seedqr_supported(self) -> bool:
|
def seedqr_supported(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bip85_supported(self) -> bool:
|
def bip85_supported(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ class SettingsConstants:
|
|||||||
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
|
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
|
||||||
LABEL__CUSTOM_EXTENSION = "Custom Extension"
|
LABEL__CUSTOM_EXTENSION = "Custom Extension"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SettingsEntry:
|
class SettingsEntry:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -620,6 +620,7 @@ class SeedBackupView(View):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
button_data = [self.VIEW_WORDS]
|
button_data = [self.VIEW_WORDS]
|
||||||
|
|
||||||
if self.seed.seedqr_supported:
|
if self.seed.seedqr_supported:
|
||||||
button_data.append(self.EXPORT_SEEDQR)
|
button_data.append(self.EXPORT_SEEDQR)
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class TestSeedFlows(FlowTest):
|
|||||||
]
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Or we bomb out if the mnemonic is invalid for Electrum
|
||||||
sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD))
|
sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD))
|
||||||
|
|
||||||
self.run_sequence(sequence)
|
self.run_sequence(sequence)
|
||||||
@@ -164,10 +165,8 @@ class TestSeedFlows(FlowTest):
|
|||||||
test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split())
|
test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split())
|
||||||
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split())
|
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split())
|
||||||
|
|
||||||
# Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide
|
# 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)
|
test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False)
|
||||||
test_with_mnemonic("gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split(), expects_electrum_seed_is_valid=False)
|
|
||||||
test_with_mnemonic("enough board blossom stamp fire buffalo digital solution sadness random number stone".split(), expects_electrum_seed_is_valid=False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_export_xpub_standard_flow(self):
|
def test_export_xpub_standard_flow(self):
|
||||||
|
|||||||
+41
-1
@@ -1,8 +1,10 @@
|
|||||||
from seedsigner.models.seed import Seed, ElectrumSeed
|
import pytest
|
||||||
|
from seedsigner.models.seed import InvalidSeedException, Seed, ElectrumSeed
|
||||||
|
|
||||||
from seedsigner.models.settings import SettingsConstants
|
from seedsigner.models.settings import SettingsConstants
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Change TAB indents to SPACE
|
||||||
|
|
||||||
def test_seed():
|
def test_seed():
|
||||||
seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split())
|
seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split())
|
||||||
@@ -37,9 +39,47 @@ def test_seed():
|
|||||||
|
|
||||||
|
|
||||||
def test_electrum_seed():
|
def test_electrum_seed():
|
||||||
|
"""
|
||||||
|
ElectrumSeed should correctly parse a modern Electrum mnemonic.
|
||||||
|
"""
|
||||||
seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split())
|
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'
|
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
|
assert seed.seed_bytes == intended_seed
|
||||||
|
|
||||||
|
|
||||||
|
def test_electrum_mnemonic_format():
|
||||||
|
"""
|
||||||
|
ElectrumSeed should reject mnemonics that are not 12 words long.
|
||||||
|
"""
|
||||||
|
with pytest.raises(InvalidSeedException):
|
||||||
|
ElectrumSeed(mnemonic=["regular"] * 11)
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSeedException):
|
||||||
|
ElectrumSeed(mnemonic=["regular"] * 13)
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSeedException):
|
||||||
|
ElectrumSeed(mnemonic=["regular"] * 24)
|
||||||
|
|
||||||
|
|
||||||
|
def test_electrum_seed_rejects_most_bip39_mnemonics():
|
||||||
|
"""
|
||||||
|
ElectrumSeed should throw an exception for most BIP-39 mnemonics.
|
||||||
|
|
||||||
|
There are 1/16 odds that a seed will be valid for both formats.
|
||||||
|
"""
|
||||||
|
# Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide
|
||||||
|
with pytest.raises(InvalidSeedException):
|
||||||
|
ElectrumSeed(mnemonic="pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split())
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSeedException):
|
||||||
|
ElectrumSeed(mnemonic="gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split())
|
||||||
|
|
||||||
|
with pytest.raises(InvalidSeedException):
|
||||||
|
ElectrumSeed(mnemonic="enough board blossom stamp fire buffalo digital solution sadness random number stone".split())
|
||||||
|
|
||||||
|
# This one is valid for both formats
|
||||||
|
mnemonic = "only gain spot output unknown craft simple cram absorb suggest ridge famous".split()
|
||||||
|
Seed(mnemonic)
|
||||||
|
ElectrumSeed(mnemonic)
|
||||||
|
|||||||
Reference in New Issue
Block a user