electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 9eb152ed98ca056d7de264dca81aa4719ba14945
parent 9b4414fb2ea87dbb3ce410fc030af0c23c0bcd79
Author: SomberNight <somber.night@protonmail.com>
Date:   Fri, 11 Sep 2020 13:09:30 +0200

keystore: improve check_password.

and add tests that exercise it

maybe fixes #4128

Diffstat:
Melectrum/bitcoin.py | 16+++++++++-------
Melectrum/keystore.py | 14++++++++++----
Melectrum/tests/test_wallet.py | 38++++++++++++++++++++++++++++++++++++--
3 files changed, 55 insertions(+), 13 deletions(-)

diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py @@ -497,6 +497,9 @@ __b43chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:' assert len(__b43chars) == 43 +class BaseDecodeError(BitcoinException): pass + + def base_encode(v: bytes, *, base: int) -> str: """ encode v, which is a string of bytes, to base58.""" assert_bytes(v) @@ -544,7 +547,7 @@ def base_decode(v: Union[bytes, str], *, base: int, length: int = None) -> Optio for c in v[::-1]: digit = chars.find(bytes([c])) if digit == -1: - raise ValueError('Forbidden character {} for base {}'.format(c, base)) + raise BaseDecodeError('Forbidden character {} for base {}'.format(c, base)) # naive but slow variant: long_value += digit * (base**i) long_value += digit * power_of_base power_of_base *= base @@ -567,7 +570,7 @@ def base_decode(v: Union[bytes, str], *, base: int, length: int = None) -> Optio return bytes(result) -class InvalidChecksum(Exception): +class InvalidChecksum(BaseDecodeError): pass @@ -633,18 +636,17 @@ def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]: raise BitcoinException('unknown script type: {}'.format(txin_type)) try: vch = DecodeBase58Check(key) - except BaseException: + except Exception as e: neutered_privkey = str(key)[:3] + '..' + str(key)[-2:] - raise BitcoinException("cannot deserialize privkey {}" - .format(neutered_privkey)) + raise BaseDecodeError(f"cannot deserialize privkey {neutered_privkey}") from e if txin_type is None: # keys exported in version 3.0.x encoded script type in first byte prefix_value = vch[0] - constants.net.WIF_PREFIX try: txin_type = WIF_SCRIPT_TYPES_INV[prefix_value] - except KeyError: - raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) + except KeyError as e: + raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) from None else: # all other keys must have a fixed first byte if vch[0] != constants.net.WIF_PREFIX: diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -32,7 +32,7 @@ from functools import lru_cache from abc import ABC, abstractmethod from . import bitcoin, ecc, constants, bip32 -from .bitcoin import deserialize_privkey, serialize_privkey +from .bitcoin import deserialize_privkey, serialize_privkey, BaseDecodeError from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, @@ -251,8 +251,10 @@ class Imported_KeyStore(Software_KeyStore): def get_private_key(self, pubkey: str, password): sec = pw_decode(self.keypairs[pubkey], password, version=self.pw_hash_version) - txin_type, privkey, compressed = deserialize_privkey(sec) - # this checks the password + try: + txin_type, privkey, compressed = deserialize_privkey(sec) + except BaseDecodeError as e: + raise InvalidPassword() from e if pubkey != ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed): raise InvalidPassword() return privkey, compressed @@ -529,7 +531,11 @@ class BIP32_KeyStore(Xpub, Deterministic_KeyStore): def check_password(self, password): xprv = pw_decode(self.xprv, password, version=self.pw_hash_version) - if BIP32Node.from_xkey(xprv).chaincode != self.get_bip32_node_for_xpub().chaincode: + try: + bip32node = BIP32Node.from_xkey(xprv) + except BaseDecodeError as e: + raise InvalidPassword() from e + if bip32node.chaincode != self.get_bip32_node_for_xpub().chaincode: raise InvalidPassword() def update_password(self, old_password, new_password): diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py @@ -10,9 +10,9 @@ from io import StringIO from electrum.storage import WalletStorage from electrum.wallet_db import FINAL_SEED_VERSION from electrum.wallet import (Abstract_Wallet, Standard_Wallet, create_new_wallet, - restore_wallet_from_text, Imported_Wallet) + restore_wallet_from_text, Imported_Wallet, Wallet) from electrum.exchange_rate import ExchangeBase, FxThread -from electrum.util import TxMinedInfo +from electrum.util import TxMinedInfo, InvalidPassword from electrum.bitcoin import COIN from electrum.wallet_db import WalletDB from electrum.simple_config import SimpleConfig @@ -229,3 +229,37 @@ class TestCreateRestoreWallet(WalletTestCase): # also test addr deletion wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c') self.assertEqual(1, len(wallet.get_receiving_addresses())) + + +class TestWalletPassword(WalletTestCase): + + def test_update_password_of_imported_wallet(self): + wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' + db = WalletDB(wallet_str, manual_upgrades=False) + storage = WalletStorage(self.wallet_path) + wallet = Wallet(db, storage, config=self.config) + + wallet.check_password(None) + + wallet.update_password(None, "1234") + + with self.assertRaises(InvalidPassword): + wallet.check_password(None) + with self.assertRaises(InvalidPassword): + wallet.check_password("wrong password") + wallet.check_password("1234") + + def test_update_password_of_standard_wallet(self): + wallet_str = '''{"addr_history":{"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes":[],"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1":[],"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB":[],"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c":[],"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz":[],"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA":[],"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV":[],"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z":[],"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv":[],"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B":[],"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz":[],"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G":[],"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq":[],"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d":[],"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs":[],"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado":[],"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z":[],"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52":[],"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP":[],"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv":[],"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb":[],"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ":[],"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G":[],"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN":[],"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J":[],"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt":[]},"addresses":{"change":["1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP","1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z","15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV","1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq","19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G","1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb"],"receiving":["14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA","13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB","19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz","1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv","1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt","13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c","1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ","12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes","12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1","14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz","1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN","17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z","1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado","18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv","1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G","18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B","1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d","1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs","1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52","1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J"]},"keystore":{"seed":"cereal wise two govern top pet frog nut rule sketch bundle logic","type":"bip32","xprv":"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v","xpub":"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[619,310,840,405]}''' + db = WalletDB(wallet_str, manual_upgrades=False) + storage = WalletStorage(self.wallet_path) + wallet = Wallet(db, storage, config=self.config) + + wallet.check_password(None) + + wallet.update_password(None, "1234") + with self.assertRaises(InvalidPassword): + wallet.check_password(None) + with self.assertRaises(InvalidPassword): + wallet.check_password("wrong password") + wallet.check_password("1234")