diff --git a/electrum/bip32.py b/electrum/bip32.py index 25eeadbd4..248abef21 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -352,7 +352,9 @@ def convert_bip32_strpath_to_intpath(n: str) -> List[int]: return path -def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str: +def convert_bip32_intpath_to_strpath(path: Sequence[int], *, hardened_char=BIP32_HARDENED_CHAR) -> str: + assert isinstance(hardened_char, str), hardened_char + assert len(hardened_char) == 1, hardened_char s = "m/" for child_index in path: if not isinstance(child_index, int): @@ -361,7 +363,7 @@ def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str: raise ValueError(f"bip32 path child index out of range: {child_index}") prime = "" if child_index & BIP32_PRIME: - prime = BIP32_HARDENED_CHAR + prime = hardened_char child_index = child_index ^ BIP32_PRIME s += str(child_index) + prime + '/' # cut trailing "/" @@ -380,13 +382,13 @@ def is_bip32_derivation(s: str) -> bool: return True -def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]: +def normalize_bip32_derivation(s: Optional[str], *, hardened_char=BIP32_HARDENED_CHAR) -> Optional[str]: if s is None: return None if not is_bip32_derivation(s): raise ValueError(f"invalid bip32 derivation: {s}") ints = convert_bip32_strpath_to_intpath(s) - return convert_bip32_intpath_to_strpath(ints) + return convert_bip32_intpath_to_strpath(ints, hardened_char=hardened_char) def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool: diff --git a/electrum/keystore.py b/electrum/keystore.py index 61305635c..fdab6f644 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -508,7 +508,9 @@ class Xpub(MasterPublicKeyMixin): return self._xpub_bip32_node def get_derivation_prefix(self) -> Optional[str]: - return self._derivation_prefix + if self._derivation_prefix is None: + return None + return normalize_bip32_derivation(self._derivation_prefix) def get_root_fingerprint(self) -> Optional[str]: return self._root_fingerprint diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 1c82b3a76..22caaa155 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -19,6 +19,7 @@ import copy from electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.bitcoin import public_key_to_p2pkh from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation +from electrum.bip32 import normalize_bip32_derivation from electrum import descriptor from electrum import ecc from electrum.ecc import msg_magic @@ -104,7 +105,8 @@ class DigitalBitbox_Client(HardwareClientBase): return False return True - def _get_xpub(self, bip32_path): + def _get_xpub(self, bip32_path: str): + bip32_path = normalize_bip32_derivation(bip32_path, hardened_char="'") if self.check_device_dialog(): return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8')) @@ -458,6 +460,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): try: message = message.encode('utf8') inputPath = self.get_derivation_prefix() + "/%d/%d" % sequence + inputPath = normalize_bip32_derivation(inputPath, hardened_char="'") msg_hash = sha256d(msg_magic(message)) inputHash = to_hexstr(msg_hash) hasharray = [] diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 0e4245f74..17c1caca4 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Sequence, Tuple from electrum import bip32, constants, ecc from electrum import descriptor from electrum.base_wizard import ScriptTypeNotSupported -from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, normalize_bip32_derivation from electrum.bitcoin import EncodeBase58Check, int_to_hex, is_b58_address, is_segwit_script_type, var_int from electrum.crypto import hash_160 from electrum.i18n import _ @@ -430,7 +430,7 @@ class Ledger_Client_Legacy(Ledger_Client): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) - bip32_path = bip32.normalize_bip32_derivation(bip32_path) + bip32_path = bip32.normalize_bip32_derivation(bip32_path, hardened_char="'") bip32_intpath = bip32.convert_bip32_strpath_to_intpath(bip32_path) bip32_path = bip32_path[2:] # cut off "m/" if len(bip32_intpath) >= 1: @@ -931,10 +931,10 @@ class Ledger_Client_New(Ledger_Client): @runs_in_hwd_thread @test_pin_unlocked - def get_xpub(self, bip32_path, xtype): + def get_xpub(self, bip32_path: str, xtype): # try silently first; if not a standard path, repeat with on-screen display - bip32_path = bip32_path.replace('h', '\'') + bip32_path = normalize_bip32_derivation(bip32_path, hardened_char="'") # cache known path/xpubs combinations in order to avoid requesting them many times if bip32_path in self._known_xpubs: @@ -1300,14 +1300,18 @@ class Ledger_KeyStore(Hardware_KeyStore): raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) def sign_message(self, sequence, *args, **kwargs): - address_path = self.get_derivation_prefix()[2:] + "/%d/%d" % sequence + address_path = self.get_derivation_prefix() + "/%d/%d" % sequence + address_path = normalize_bip32_derivation(address_path, hardened_char="'") + address_path = address_path[2:] # cut m/ return self.get_client_dongle_object().sign_message(address_path, *args, **kwargs) def sign_transaction(self, *args, **kwargs): return self.get_client_dongle_object().sign_transaction(self, *args, **kwargs) def show_address(self, sequence, *args, **kwargs): - address_path = self.get_derivation_prefix()[2:] + "/%d/%d" % sequence + address_path = self.get_derivation_prefix() + "/%d/%d" % sequence + address_path = normalize_bip32_derivation(address_path, hardened_char="'") + address_path = address_path[2:] # cut m/ return self.get_client_dongle_object().show_address(address_path, *args, **kwargs) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 5699902c4..0a8d29741 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -862,6 +862,10 @@ class Test_xprv_xpub(ElectrumTestCase): self.assertEqual("m", convert_bip32_intpath_to_strpath([])) self.assertEqual("m/44h/5241h/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221])) + self.assertEqual("m/0/1'/1'", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001], hardened_char="'")) + self.assertEqual("m", convert_bip32_intpath_to_strpath([], hardened_char="'")) + self.assertEqual("m/44'/5241'/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221], hardened_char="'")) + def test_normalize_bip32_derivation(self): self.assertEqual("m/0/1h/1h", normalize_bip32_derivation("m/0/1h/1'")) self.assertEqual("m", normalize_bip32_derivation("m////"))