From 499f51535ffa54607fd75eb4bf817cb96bd0e0cd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 17:03:16 +0000 Subject: [PATCH] bip32: fix hardened char "h" vs "'" compatibility for some hw wallets 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 return [int(p).to_bytes(4, byteorder="big") if "'" not in p ValueError: invalid literal for int() with base 10: '84h' ``` Regression from df2bd61de6607fa8bbc73cbf53ee1d99fac61bfd, 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. --- electrum/bip32.py | 10 ++++++---- electrum/keystore.py | 4 +++- electrum/plugins/digitalbitbox/digitalbitbox.py | 5 ++++- electrum/plugins/ledger/ledger.py | 16 ++++++++++------ electrum/tests/test_bitcoin.py | 4 ++++ 5 files changed, 27 insertions(+), 12 deletions(-) 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////"))