Browse Source

keystore.check_password: raise better exc if called on pwless ks

If keystore.check_password is called with some pw on a keystore that does not have a password set,
it now raises better exceptions: it should now always raise InvalidPassword, and with a nicer msg.
Previously the exc type would depend on the ks type.

Examples before change:

```
>>> wallet.keystore.check_password("asd")
Traceback (most recent call last):
  File "/home/user/wspace/electrum/electrum/keystore.py", line 580, in check_password
    xprv = pw_decode(self.xprv, password, version=self.pw_hash_version)
  File "/home/user/wspace/electrum/electrum/crypto.py", line 311, in pw_decode
    plaintext_bytes = pw_decode_bytes(data, password, version=version)
  File "/home/user/wspace/electrum/electrum/crypto.py", line 270, in pw_decode_bytes
    data_bytes = bytes(base64.b64decode(data))
  File "/usr/lib/python3.10/base64.py", line 87, in b64decode
    return binascii.a2b_base64(s)
binascii.Error: Incorrect padding
```

```
>>> wallet.keystore.check_password("asd")
Traceback (most recent call last):
    s = aes_decrypt_with_iv(secret, iv, e)
  File "/home/user/wspace/electrum/electrum/crypto.py", line 157, in aes_decrypt_with_iv
    data = decryptor.update(data) + decryptor.finalize()
  File "/usr/lib/python3/dist-packages/cryptography/hazmat/primitives/ciphers/base.py", line 148, in finalize
    data = self._ctx.finalize()
  File "/usr/lib/python3/dist-packages/cryptography/hazmat/backends/openssl/ciphers.py", line 193, in finalize
    raise ValueError(
ValueError: The length of the provided data is not a multiple of the block length.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/user/wspace/electrum/electrum/gui/qt/console.py", line 254, in exec_command
    result = eval(command, self.namespace, self.namespace)
  File "<string>", line 1, in <module>
  File "/home/user/wspace/electrum/electrum/keystore.py", line 248, in check_password
    self.get_private_key(pubkey, password)
  File "/home/user/wspace/electrum/electrum/keystore.py", line 267, in get_private_key
    sec = pw_decode(self.keypairs[pubkey], password, version=self.pw_hash_version)
  File "/home/user/wspace/electrum/electrum/crypto.py", line 311, in pw_decode
    plaintext_bytes = pw_decode_bytes(data, password, version=version)
  File "/home/user/wspace/electrum/electrum/crypto.py", line 271, in pw_decode_bytes
    return _pw_decode_raw(data_bytes, password, version=version)
  File "/home/user/wspace/electrum/electrum/crypto.py", line 255, in _pw_decode_raw
    raise InvalidPassword() from e
electrum.util.InvalidPassword: Incorrect password
```

-----

Examples after change:
```
>>> wallet.keystore.check_password("asd")
Traceback (most recent call last):
    return binascii.a2b_base64(s)
binascii.Error: Incorrect padding

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "...\electrum\keystore.py", line 68, in wrapper
    return check_password_fn(self, password)
  File "...\electrum\keystore.py", line 605, in check_password
    xprv = pw_decode(self.xprv, password, version=self.pw_hash_version)
  File "...\electrum\crypto.py", line 311, in pw_decode
    plaintext_bytes = pw_decode_bytes(data, password, version=version)
  File "...\electrum\crypto.py", line 267, in pw_decode_bytes
    raise CiphertextFormatError("ciphertext not valid base64") from e
electrum.crypto.CiphertextFormatError: ciphertext not valid base64

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "...\electrum\gui\qt\console.py", line 254, in exec_command
    result = eval(command, self.namespace, self.namespace)
  File "<string>", line 1, in <module>
  File "...\electrum\keystore.py", line 76, in wrapper
    raise InvalidPassword("password given but keystore has no password") from e
electrum.util.InvalidPassword: password given but keystore has no password
```

```
>>> wallet.keystore.check_password("asd")
Traceback (most recent call last):
    s = aes_decrypt_with_iv(secret, iv, e)
  File "...\electrum\crypto.py", line 158, in aes_decrypt_with_iv
    data = cipher.decrypt(data)
  File "...\Python310\site-packages\Cryptodome\Cipher\_mode_cbc.py", line 246, in decrypt
    raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
ValueError: Data must be padded to 16 byte boundary in CBC mode

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "...\electrum\keystore.py", line 68, in wrapper
    return check_password_fn(self, password)
  File "...\electrum\keystore.py", line 272, in check_password
    self.get_private_key(pubkey, password)
  File "...\electrum\keystore.py", line 291, in get_private_key
    sec = pw_decode(self.keypairs[pubkey], password, version=self.pw_hash_version)
  File "...\electrum\crypto.py", line 311, in pw_decode
    plaintext_bytes = pw_decode_bytes(data, password, version=version)
  File "...\electrum\crypto.py", line 268, in pw_decode_bytes
    return _pw_decode_raw(data_bytes, password, version=version)
  File "...\electrum\crypto.py", line 249, in _pw_decode_raw
    raise InvalidPassword() from e
electrum.util.InvalidPassword: Incorrect password

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "...\electrum\gui\qt\console.py", line 254, in exec_command
    result = eval(command, self.namespace, self.namespace)
  File "<string>", line 1, in <module>
  File "...\electrum\keystore.py", line 76, in wrapper
    raise InvalidPassword("password given but keystore has no password") from e
electrum.util.InvalidPassword: password given but keystore has no password
```
patch-4
SomberNight 3 years ago
parent
commit
28fe345b0b
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 15
      electrum/crypto.py
  2. 32
      electrum/keystore.py
  3. 8
      electrum/util.py

15
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:]

32
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)

8
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):

Loading…
Cancel
Save