diff --git a/electrum/crypto.py b/electrum/crypto.py index f948736f1..4b50d6825 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -24,6 +24,7 @@ # SOFTWARE. import base64 +import binascii import os import sys import hashlib @@ -109,6 +110,10 @@ class InvalidPadding(Exception): pass +class CiphertextFormatError(Exception): + pass + + def append_PKCS7_padding(data: bytes) -> bytes: assert_bytes(data) padlen = 16 - (len(data) % 16) @@ -256,7 +261,10 @@ def pw_decode_bytes(data: str, password: Union[bytes, str], *, version:int) -> b """base64 ciphertext -> plaintext bytes""" if version not in KNOWN_PW_HASH_VERSIONS: raise UnexpectedPasswordHashVersion(version) - data_bytes = bytes(base64.b64decode(data)) + try: + data_bytes = bytes(base64.b64decode(data, validate=True)) + except binascii.Error as e: + raise CiphertextFormatError("ciphertext not valid base64") from e return _pw_decode_raw(data_bytes, password, version=version) @@ -273,7 +281,10 @@ def pw_encode_with_version_and_mac(data: bytes, password: Union[bytes, str]) -> def pw_decode_with_version_and_mac(data: str, password: Union[bytes, str]) -> bytes: """base64 ciphertext -> plaintext bytes""" - data_bytes = bytes(base64.b64decode(data)) + try: + data_bytes = bytes(base64.b64decode(data, validate=True)) + except binascii.Error as e: + raise CiphertextFormatError("ciphertext not valid base64") from e version = int(data_bytes[0]) encrypted = data_bytes[1:-4] mac = data_bytes[-4:] diff --git a/electrum/keystore.py b/electrum/keystore.py index 578e198de..353f33022 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -28,7 +28,7 @@ from unicodedata import normalize import hashlib import re from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple -from functools import lru_cache +from functools import lru_cache, wraps from abc import ABC, abstractmethod from . import bitcoin, ecc, constants, bip32 @@ -39,7 +39,8 @@ from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info) from .ecc import string_to_number from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, - SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160) + SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160, + CiphertextFormatError) from .util import (InvalidPassword, WalletFileException, BitcoinException, bh2u, bfh, inv_dict, is_hex_str) from .mnemonic import Mnemonic, Wordlist, seed_type, is_seed @@ -56,6 +57,27 @@ if TYPE_CHECKING: class CannotDerivePubkey(Exception): pass +def also_test_none_password(check_password_fn): + """Decorator for check_password, simply to give a friendlier exception if + check_password(x) is called on a keystore that does not have a password set. + """ + @wraps(check_password_fn) + def wrapper(self: 'Software_KeyStore', *args): + password = args[0] + try: + return check_password_fn(self, password) + except (CiphertextFormatError, InvalidPassword) as e: + if password is not None: + try: + check_password_fn(self, None) + except Exception: + pass + else: + raise InvalidPassword("password given but keystore has no password") from e + raise + return wrapper + + class KeyStore(Logger, ABC): type: str @@ -212,7 +234,8 @@ class Software_KeyStore(KeyStore): pass @abstractmethod - def check_password(self, password): + def check_password(self, password: Optional[str]) -> None: + """Raises InvalidPassword if password is not correct""" pass @abstractmethod @@ -243,6 +266,7 @@ class Imported_KeyStore(Software_KeyStore): def can_import(self): return True + @also_test_none_password def check_password(self, password): pubkey = list(self.keypairs.keys())[0] self.get_private_key(pubkey, password) @@ -576,6 +600,7 @@ class BIP32_KeyStore(Xpub, Deterministic_KeyStore): def get_master_private_key(self, password): return pw_decode(self.xprv, password, version=self.pw_hash_version) + @also_test_none_password def check_password(self, password): xprv = pw_decode(self.xprv, password, version=self.pw_hash_version) try: @@ -742,6 +767,7 @@ class Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore): if master_public_key != bfh(self.mpk): raise InvalidPassword() + @also_test_none_password def check_password(self, password): seed = self.get_hex_seed(password) self._check_seed(seed) diff --git a/electrum/util.py b/electrum/util.py index 157106097..b82a650bb 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -144,8 +144,14 @@ class NoDynamicFeeEstimates(Exception): class InvalidPassword(Exception): + def __init__(self, message: Optional[str] = None): + self.message = message + def __str__(self): - return _("Incorrect password") + if self.message is None: + return _("Incorrect password") + else: + return str(self.message) class AddTransactionException(Exception):