diff --git a/electrum/bip32.py b/electrum/bip32.py index f07a6cfd7..87d50e849 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -372,6 +372,18 @@ def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]: return convert_bip32_intpath_to_strpath(ints) +def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool: + """Returns whether all levels in path use non-hardened derivation.""" + if isinstance(path, str): + path = convert_bip32_path_to_list_of_uint32(path) + for child_index in path: + if child_index < 0: + raise ValueError('the bip32 index needs to be non-negative') + if child_index & BIP32_PRIME: + return False + return True + + def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]: """Returns the root bip32 fingerprint and the derivation path from the root to the given xkey, if they can be determined. Otherwise (None, None). diff --git a/electrum/keystore.py b/electrum/keystore.py index 28de738bd..6624c4c64 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -707,8 +707,10 @@ class Hardware_KeyStore(KeyStore, Xpub): def opportunistically_fill_in_missing_info_from_device(self, client): assert client is not None if self._root_fingerprint is None: - root_xpub = client.get_xpub('m', xtype='standard') - root_fingerprint = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower() + # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths + # so ask for a direct child, and read out fingerprint from that: + child_of_root_xpub = client.get_xpub("m/0'", xtype='standard') + root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower() self._root_fingerprint = root_fingerprint self.is_requesting_to_be_rewritten_to_wallet_file = True diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index f1a612a1a..a85ce9156 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -19,7 +19,7 @@ import copy from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address) -from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation from electrum import ecc from electrum.ecc import msg_magic from electrum.wallet import Standard_Wallet @@ -741,6 +741,8 @@ class DigitalBitboxPlugin(HW_PluginBase): def get_xpub(self, device_id, derivation, xtype, wizard): if xtype not in self.SUPPORTED_XTYPES: raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + if is_all_public_derivation(derivation): + raise Exception(f"The {self.device} does not reveal xpubs corresponding to non-hardened paths. (path: {derivation})") devmgr = self.device_manager() client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 474e7c63d..4875ee616 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -12,7 +12,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, is_xpub, convert_bip32_path_to_list_of_uint32, - normalize_bip32_derivation) + normalize_bip32_derivation, is_all_public_derivation) from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS from electrum import ecc, crypto, constants from electrum.ecc import number_to_string, string_to_number @@ -494,6 +494,14 @@ class Test_xprv_xpub(ElectrumTestCase): self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/")) self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h")) + def test_is_all_public_derivation(self): + self.assertFalse(is_all_public_derivation("m/0/1'/1'")) + self.assertFalse(is_all_public_derivation("m/0/2/1'")) + self.assertFalse(is_all_public_derivation("m/0/1'/1'/5")) + self.assertTrue(is_all_public_derivation("m")) + self.assertTrue(is_all_public_derivation("m/0")) + self.assertTrue(is_all_public_derivation("m/75/22/3")) + def test_xtype_from_derivation(self): self.assertEqual('standard', xtype_from_derivation("m/44'")) self.assertEqual('standard', xtype_from_derivation("m/44'/"))