Skip to content

Commit 9e21b76

Browse files
committed
wallet: stricter validation in export_private_key
fixes #5422
1 parent c7b64f4 commit 9e21b76

File tree

4 files changed

+62
-8
lines changed

4 files changed

+62
-8
lines changed

electrum/bip32.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ def is_private(self) -> bool:
200200
return isinstance(self.eckey, ecc.ECPrivkey)
201201

202202
def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node':
203+
if path is None:
204+
raise Exception("derivation path must not be None")
203205
if isinstance(path, str):
204206
path = convert_bip32_path_to_list_of_uint32(path)
205207
if not self.is_private():
@@ -224,6 +226,8 @@ def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP3
224226
child_number=child_number)
225227

226228
def subkey_at_public_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node':
229+
if path is None:
230+
raise Exception("derivation path must not be None")
227231
if isinstance(path, str):
228232
path = convert_bip32_path_to_list_of_uint32(path)
229233
if not path:

electrum/tests/test_commands.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,43 @@ def test_encrypt_decrypt(self, mock_write):
7575
ciphertext = cmds.encrypt(pubkey, cleartext)
7676
self.assertEqual(cleartext, cmds.decrypt(pubkey, ciphertext))
7777

78+
@mock.patch.object(storage.WalletStorage, '_write')
79+
def test_export_private_key_imported(self, mock_write):
80+
wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',
81+
path='if_this_exists_mocking_failed_648151893')['wallet']
82+
cmds = Commands(config=None, wallet=wallet, network=None)
83+
# single address tests
84+
with self.assertRaises(Exception):
85+
cmds.getprivatekeys("asdasd") # invalid addr, though might raise "not in wallet"
86+
with self.assertRaises(Exception):
87+
cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23") # not in wallet
88+
self.assertEqual("p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL",
89+
cmds.getprivatekeys("bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw"))
90+
# list of addresses tests
91+
with self.assertRaises(Exception):
92+
cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd'])
93+
self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],
94+
cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n']))
95+
96+
@mock.patch.object(storage.WalletStorage, '_write')
97+
def test_export_private_key_deterministic(self, mock_write):
98+
wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver',
99+
gap_limit=2,
100+
path='if_this_exists_mocking_failed_648151893')['wallet']
101+
cmds = Commands(config=None, wallet=wallet, network=None)
102+
# single address tests
103+
with self.assertRaises(Exception):
104+
cmds.getprivatekeys("asdasd") # invalid addr, though might raise "not in wallet"
105+
with self.assertRaises(Exception):
106+
cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23") # not in wallet
107+
self.assertEqual("p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2",
108+
cmds.getprivatekeys("bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af"))
109+
# list of addresses tests
110+
with self.assertRaises(Exception):
111+
cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd'])
112+
self.assertEqual(['p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],
113+
cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n']))
114+
78115

79116
class TestCommandsTestnet(TestCaseForTestnet):
80117

electrum/tests/test_wallet.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ def test_create_new_wallet(self):
156156
passphrase=passphrase,
157157
password=password,
158158
encrypt_file=encrypt_file,
159-
segwit=True)
159+
segwit=True,
160+
gap_limit=1)
160161
wallet = d['wallet'] # type: Standard_Wallet
161162
wallet.check_password(password)
162163
self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))
@@ -173,7 +174,8 @@ def test_restore_wallet_from_text_mnemonic(self):
173174
network=None,
174175
passphrase=passphrase,
175176
password=password,
176-
encrypt_file=encrypt_file)
177+
encrypt_file=encrypt_file,
178+
gap_limit=1)
177179
wallet = d['wallet'] # type: Standard_Wallet
178180
self.assertEqual(passphrase, wallet.keystore.get_passphrase(password))
179181
self.assertEqual(text, wallet.keystore.get_seed(password))
@@ -182,14 +184,14 @@ def test_restore_wallet_from_text_mnemonic(self):
182184

183185
def test_restore_wallet_from_text_xpub(self):
184186
text = 'zpub6nydoME6CFdJtMpzHW5BNoPz6i6XbeT9qfz72wsRqGdgGEYeivso6xjfw8cGcCyHwF7BNW4LDuHF35XrZsovBLWMF4qXSjmhTXYiHbWqGLt'
185-
d = restore_wallet_from_text(text, path=self.wallet_path, network=None)
187+
d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1)
186188
wallet = d['wallet'] # type: Standard_Wallet
187189
self.assertEqual(text, wallet.keystore.get_master_public_key())
188190
self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])
189191

190192
def test_restore_wallet_from_text_xprv(self):
191193
text = 'zprvAZzHPqhCMt51fskXBUYB1fTFYgG3CBjJUT4WEZTpGw6hPSDWBPZYZARC5sE9xAcX8NeWvvucFws8vZxEa65RosKAhy7r5MsmKTxr3hmNmea'
192-
d = restore_wallet_from_text(text, path=self.wallet_path, network=None)
194+
d = restore_wallet_from_text(text, path=self.wallet_path, network=None, gap_limit=1)
193195
wallet = d['wallet'] # type: Standard_Wallet
194196
self.assertEqual(text, wallet.keystore.get_master_private_key(password=None))
195197
self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0])

electrum/wallet.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ def get_redeem_script(self, address):
346346

347347
def export_private_key(self, address, password):
348348
if self.is_watching_only():
349-
return []
349+
raise Exception(_("This is a watching-only wallet"))
350+
if not self.is_mine(address):
351+
raise Exception(_('Address not in wallet.') + f' {address}')
350352
index = self.get_address_index(address)
351353
pk, compressed = self.keystore.get_private_key(index, password)
352354
txin_type = self.get_txin_type(address)
@@ -1485,7 +1487,9 @@ def is_mine(self, address):
14851487
return self.db.has_imported_address(address)
14861488

14871489
def get_address_index(self, address):
1488-
# returns None is address is not mine
1490+
# returns None if address is not mine
1491+
if not is_address(address):
1492+
raise Exception(f"Invalid bitcoin address: {address}")
14891493
return self.get_public_key(address)
14901494

14911495
def get_public_key(self, address):
@@ -1677,6 +1681,8 @@ def is_beyond_limit(self, address):
16771681
return True
16781682

16791683
def get_address_index(self, address):
1684+
if not is_address(address):
1685+
raise Exception(f"Invalid bitcoin address: {address}")
16801686
return self.db.get_address_index(address)
16811687

16821688
def get_master_public_keys(self):
@@ -1875,7 +1881,7 @@ def wallet_class(wallet_type):
18751881
raise WalletFileException("Unknown wallet type: " + str(wallet_type))
18761882

18771883

1878-
def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True, segwit=True):
1884+
def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True, segwit=True, gap_limit=None):
18791885
"""Create a new wallet"""
18801886
storage = WalletStorage(path)
18811887
if storage.file_exists():
@@ -1886,6 +1892,8 @@ def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True
18861892
k = keystore.from_seed(seed, passphrase)
18871893
storage.put('keystore', k.dump())
18881894
storage.put('wallet_type', 'standard')
1895+
if gap_limit is not None:
1896+
storage.put('gap_limit', gap_limit)
18891897
wallet = Wallet(storage)
18901898
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
18911899
wallet.synchronize()
@@ -1896,7 +1904,8 @@ def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True
18961904

18971905

18981906
def restore_wallet_from_text(text, *, path, network=None,
1899-
passphrase=None, password=None, encrypt_file=True):
1907+
passphrase=None, password=None, encrypt_file=True,
1908+
gap_limit=None):
19001909
"""Restore a wallet from text. Text can be a seed phrase, a master
19011910
public key, a master private key, a list of bitcoin addresses
19021911
or bitcoin private keys."""
@@ -1930,6 +1939,8 @@ def restore_wallet_from_text(text, *, path, network=None,
19301939
raise Exception("Seed or key not recognized")
19311940
storage.put('keystore', k.dump())
19321941
storage.put('wallet_type', 'standard')
1942+
if gap_limit is not None:
1943+
storage.put('gap_limit', gap_limit)
19331944
wallet = Wallet(storage)
19341945

19351946
assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"

0 commit comments

Comments
 (0)