Skip to content

Commit

Permalink
bip32: fix hardened char "h" vs "'" compatibility for some hw wallets
Browse files Browse the repository at this point in the history
in particular, ledger: fix sign_message for some wallets

```
156.02 | E | plugins.ledger.ledger |
Traceback (most recent call last):
  File "...\electrum\electrum\plugins\ledger\ledger.py", line 1265, in sign_message
    result = base64.b64decode(self.client.sign_message(message, address_path))
  File "...\Python310\site-packages\ledger_bitcoin\client.py", line 230, in sign_message
    sw, response = self._make_request(self.builder.sign_message(message_bytes, bip32_path), client_intepreter)
  File "...\Python310\site-packages\ledger_bitcoin\command_builder.py", line 176, in sign_message
    bip32_path: List[bytes] = bip32_path_from_string(bip32_path)
  File "...\Python310\site-packages\ledger_bitcoin\common.py", line 68, in bip32_path_from_string
    return [int(p).to_bytes(4, byteorder="big") if "'" not in p
  File "...\Python310\site-packages\ledger_bitcoin\common.py", line 68, in <listcomp>
    return [int(p).to_bytes(4, byteorder="big") if "'" not in p
ValueError: invalid literal for int() with base 10: '84h'
```

Regression from df2bd61, where the
default hardened char was changed from "'" to "h". Note that there was
no corresponding wallet db upgrade, so some files use one char and
others use the other.
  • Loading branch information
SomberNight committed Apr 27, 2023
1 parent 9e1bb94 commit 499f515
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 12 deletions.
10 changes: 6 additions & 4 deletions electrum/bip32.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 "/"
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion electrum/keystore.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,9 @@ def get_bip32_node_for_xpub(self) -> Optional[BIP32Node]:
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
Expand Down
5 changes: 4 additions & 1 deletion electrum/plugins/digitalbitbox/digitalbitbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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
Expand Down Expand Up @@ -104,7 +105,8 @@ def has_usable_connection_with_device(self):
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'))

Expand Down Expand Up @@ -458,6 +460,7 @@ def sign_message(self, sequence, message, password, *, script_type=None):
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 = []
Expand Down
16 changes: 10 additions & 6 deletions electrum/plugins/ledger/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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 _
Expand Down Expand Up @@ -430,7 +430,7 @@ def get_xpub(self, bip32_path, xtype):
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:
Expand Down Expand Up @@ -931,10 +931,10 @@ def has_usable_connection_with_device(self):

@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:
Expand Down Expand Up @@ -1300,14 +1300,18 @@ def decrypt_message(self, pubkey, message, password):
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)


Expand Down
4 changes: 4 additions & 0 deletions electrum/tests/test_bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,10 @@ def test_convert_bip32_intpath_to_strpath(self):
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////"))
Expand Down

0 comments on commit 499f515

Please sign in to comment.