2018-05-24 18:57:13 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
#
|
|
|
|
|
# Electrum - lightweight Bitcoin client
|
|
|
|
|
# Copyright (C) 2018 The Electrum developers
|
|
|
|
|
#
|
|
|
|
|
# Permission is hereby granted, free of charge, to any person
|
|
|
|
|
# obtaining a copy of this software and associated documentation files
|
|
|
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
|
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
|
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
|
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
|
|
|
# subject to the following conditions:
|
|
|
|
|
#
|
|
|
|
|
# The above copyright notice and this permission notice shall be
|
|
|
|
|
# included in all copies or substantial portions of the Software.
|
|
|
|
|
#
|
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
|
|
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
|
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
# SOFTWARE.
|
|
|
|
|
|
|
|
|
|
import base64
|
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
```
2022-07-12 15:31:03 +02:00
|
|
|
import binascii
|
2018-05-24 18:57:13 +02:00
|
|
|
import os
|
2020-03-04 18:54:20 +01:00
|
|
|
import sys
|
2018-05-24 18:57:13 +02:00
|
|
|
import hashlib
|
2018-06-28 11:42:47 +02:00
|
|
|
import hmac
|
commands: add "version_info" cmd
example:
```
$ ./run_electrum -o version_info
{
"aiohttp.version": "3.8.1",
"aiorpcx.version": "0.22.1",
"certifi.version": "2021.10.08",
"cryptodome.version": null,
"cryptography.path": "/home/user/.local/lib/python3.8/site-packages/cryptography",
"cryptography.version": "3.4.6",
"dnspython.version": "2.2.0",
"electrum.path": "/home/user/wspace/electrum/electrum",
"electrum.version": "4.2.1",
"hidapi.version": "0.11.0.post2",
"libsecp256k1.path": "/home/user/wspace/electrum/electrum/libsecp256k1.so.0",
"libusb.path": "libusb-1.0.so",
"libusb.version": "1.0.23.11397",
"libzbar.path": "/home/user/wspace/electrum/electrum/libzbar.so.0",
"pyaes.version": "1.3.0",
"pyqt.path": "/usr/lib/python3/dist-packages/PyQt5",
"pyqt.version": "5.14.1",
"qt.version": "5.12.8"
}
```
2022-04-11 17:05:26 +02:00
|
|
|
from typing import Union, Mapping, Optional
|
2018-05-24 18:57:13 +02:00
|
|
|
|
2024-06-17 13:38:54 +02:00
|
|
|
import electrum_ecc as ecc
|
|
|
|
|
|
2020-10-15 16:25:06 +02:00
|
|
|
from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException, versiontuple
|
2018-11-10 15:30:41 +01:00
|
|
|
from .i18n import _
|
2020-10-15 16:25:06 +02:00
|
|
|
from .logging import get_logger
|
|
|
|
|
|
|
|
|
|
_logger = get_logger(__name__)
|
2018-05-24 18:57:13 +02:00
|
|
|
|
|
|
|
|
|
2020-09-07 18:07:41 +02:00
|
|
|
HAS_PYAES = False
|
|
|
|
|
try:
|
|
|
|
|
import pyaes
|
2023-04-23 01:33:12 +00:00
|
|
|
except Exception:
|
2020-09-07 18:07:41 +02:00
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
HAS_PYAES = True
|
|
|
|
|
|
2020-03-04 18:54:20 +01:00
|
|
|
HAS_CRYPTODOME = False
|
2020-10-15 16:25:06 +02:00
|
|
|
MIN_CRYPTODOME_VERSION = "3.7"
|
2018-05-24 18:57:13 +02:00
|
|
|
try:
|
2020-10-15 16:25:06 +02:00
|
|
|
import Cryptodome
|
|
|
|
|
if versiontuple(Cryptodome.__version__) < versiontuple(MIN_CRYPTODOME_VERSION):
|
|
|
|
|
_logger.warning(f"found module 'Cryptodome' but it is too old: {Cryptodome.__version__}<{MIN_CRYPTODOME_VERSION}")
|
|
|
|
|
raise Exception()
|
2020-03-04 18:54:20 +01:00
|
|
|
from Cryptodome.Cipher import ChaCha20_Poly1305 as CD_ChaCha20_Poly1305
|
|
|
|
|
from Cryptodome.Cipher import ChaCha20 as CD_ChaCha20
|
|
|
|
|
from Cryptodome.Cipher import AES as CD_AES
|
2023-04-23 01:33:12 +00:00
|
|
|
except Exception:
|
2020-03-04 18:54:20 +01:00
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
HAS_CRYPTODOME = True
|
|
|
|
|
|
|
|
|
|
HAS_CRYPTOGRAPHY = False
|
2020-10-15 16:25:06 +02:00
|
|
|
MIN_CRYPTOGRAPHY_VERSION = "2.1"
|
2020-03-04 18:54:20 +01:00
|
|
|
try:
|
|
|
|
|
import cryptography
|
2020-10-15 16:25:06 +02:00
|
|
|
if versiontuple(cryptography.__version__) < versiontuple(MIN_CRYPTOGRAPHY_VERSION):
|
|
|
|
|
_logger.warning(f"found module 'cryptography' but it is too old: {cryptography.__version__}<{MIN_CRYPTOGRAPHY_VERSION}")
|
|
|
|
|
raise Exception()
|
2020-03-04 18:54:20 +01:00
|
|
|
from cryptography import exceptions
|
|
|
|
|
from cryptography.hazmat.primitives.ciphers import Cipher as CG_Cipher
|
|
|
|
|
from cryptography.hazmat.primitives.ciphers import algorithms as CG_algorithms
|
|
|
|
|
from cryptography.hazmat.primitives.ciphers import modes as CG_modes
|
|
|
|
|
from cryptography.hazmat.backends import default_backend as CG_default_backend
|
|
|
|
|
import cryptography.hazmat.primitives.ciphers.aead as CG_aead
|
2023-04-23 01:33:12 +00:00
|
|
|
except Exception:
|
2020-03-04 18:54:20 +01:00
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
HAS_CRYPTOGRAPHY = True
|
|
|
|
|
|
2018-05-24 18:57:13 +02:00
|
|
|
|
2020-03-04 18:54:20 +01:00
|
|
|
if not (HAS_CRYPTODOME or HAS_CRYPTOGRAPHY):
|
|
|
|
|
sys.exit(f"Error: at least one of ('pycryptodomex', 'cryptography') needs to be installed.")
|
2020-03-04 16:15:22 +01:00
|
|
|
|
2018-05-24 18:57:13 +02:00
|
|
|
|
commands: add "version_info" cmd
example:
```
$ ./run_electrum -o version_info
{
"aiohttp.version": "3.8.1",
"aiorpcx.version": "0.22.1",
"certifi.version": "2021.10.08",
"cryptodome.version": null,
"cryptography.path": "/home/user/.local/lib/python3.8/site-packages/cryptography",
"cryptography.version": "3.4.6",
"dnspython.version": "2.2.0",
"electrum.path": "/home/user/wspace/electrum/electrum",
"electrum.version": "4.2.1",
"hidapi.version": "0.11.0.post2",
"libsecp256k1.path": "/home/user/wspace/electrum/electrum/libsecp256k1.so.0",
"libusb.path": "libusb-1.0.so",
"libusb.version": "1.0.23.11397",
"libzbar.path": "/home/user/wspace/electrum/electrum/libzbar.so.0",
"pyaes.version": "1.3.0",
"pyqt.path": "/usr/lib/python3/dist-packages/PyQt5",
"pyqt.version": "5.14.1",
"qt.version": "5.12.8"
}
```
2022-04-11 17:05:26 +02:00
|
|
|
def version_info() -> Mapping[str, Optional[str]]:
|
|
|
|
|
ret = {}
|
|
|
|
|
if HAS_PYAES:
|
|
|
|
|
ret["pyaes.version"] = ".".join(map(str, pyaes.VERSION[:3]))
|
|
|
|
|
else:
|
|
|
|
|
ret["pyaes.version"] = None
|
|
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
ret["cryptodome.version"] = Cryptodome.__version__
|
|
|
|
|
if hasattr(Cryptodome, "__path__"):
|
|
|
|
|
ret["cryptodome.path"] = ", ".join(Cryptodome.__path__ or [])
|
|
|
|
|
else:
|
|
|
|
|
ret["cryptodome.version"] = None
|
|
|
|
|
if HAS_CRYPTOGRAPHY:
|
|
|
|
|
ret["cryptography.version"] = cryptography.__version__
|
|
|
|
|
if hasattr(cryptography, "__path__"):
|
|
|
|
|
ret["cryptography.path"] = ", ".join(cryptography.__path__ or [])
|
|
|
|
|
else:
|
|
|
|
|
ret["cryptography.version"] = None
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
2018-05-24 18:57:13 +02:00
|
|
|
class InvalidPadding(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
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
```
2022-07-12 15:31:03 +02:00
|
|
|
class CiphertextFormatError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-11-10 13:30:34 +01:00
|
|
|
def append_PKCS7_padding(data: bytes) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
assert_bytes(data)
|
|
|
|
|
padlen = 16 - (len(data) % 16)
|
|
|
|
|
return data + bytes([padlen]) * padlen
|
|
|
|
|
|
|
|
|
|
|
2018-11-10 13:30:34 +01:00
|
|
|
def strip_PKCS7_padding(data: bytes) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
assert_bytes(data)
|
|
|
|
|
if len(data) % 16 != 0 or len(data) == 0:
|
|
|
|
|
raise InvalidPadding("invalid length")
|
|
|
|
|
padlen = data[-1]
|
2018-11-22 18:21:19 +01:00
|
|
|
if not (0 < padlen <= 16):
|
|
|
|
|
raise InvalidPadding("invalid padding byte (out of range)")
|
2018-05-24 18:57:13 +02:00
|
|
|
for i in data[-padlen:]:
|
|
|
|
|
if i != padlen:
|
|
|
|
|
raise InvalidPadding("invalid padding byte (inconsistent)")
|
|
|
|
|
return data[0:-padlen]
|
|
|
|
|
|
|
|
|
|
|
2018-11-10 13:30:34 +01:00
|
|
|
def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
assert_bytes(key, iv, data)
|
|
|
|
|
data = append_PKCS7_padding(data)
|
2020-03-04 18:54:20 +01:00
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
e = CD_AES.new(key, CD_AES.MODE_CBC, iv).encrypt(data)
|
|
|
|
|
elif HAS_CRYPTOGRAPHY:
|
|
|
|
|
cipher = CG_Cipher(CG_algorithms.AES(key), CG_modes.CBC(iv), backend=CG_default_backend())
|
|
|
|
|
encryptor = cipher.encryptor()
|
|
|
|
|
e = encryptor.update(data) + encryptor.finalize()
|
2020-09-07 18:07:41 +02:00
|
|
|
elif HAS_PYAES:
|
2018-05-24 18:57:13 +02:00
|
|
|
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
|
|
|
|
|
aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE)
|
|
|
|
|
e = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer
|
2020-09-07 18:07:41 +02:00
|
|
|
else:
|
|
|
|
|
raise Exception("no AES backend found")
|
2018-05-24 18:57:13 +02:00
|
|
|
return e
|
|
|
|
|
|
|
|
|
|
|
2018-11-10 13:30:34 +01:00
|
|
|
def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
assert_bytes(key, iv, data)
|
2020-03-04 18:54:20 +01:00
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
cipher = CD_AES.new(key, CD_AES.MODE_CBC, iv)
|
2018-05-24 18:57:13 +02:00
|
|
|
data = cipher.decrypt(data)
|
2020-03-04 18:54:20 +01:00
|
|
|
elif HAS_CRYPTOGRAPHY:
|
|
|
|
|
cipher = CG_Cipher(CG_algorithms.AES(key), CG_modes.CBC(iv), backend=CG_default_backend())
|
|
|
|
|
decryptor = cipher.decryptor()
|
|
|
|
|
data = decryptor.update(data) + decryptor.finalize()
|
2020-09-07 18:07:41 +02:00
|
|
|
elif HAS_PYAES:
|
2018-05-24 18:57:13 +02:00
|
|
|
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
|
|
|
|
|
aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE)
|
|
|
|
|
data = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer
|
2020-09-07 18:07:41 +02:00
|
|
|
else:
|
|
|
|
|
raise Exception("no AES backend found")
|
2018-05-24 18:57:13 +02:00
|
|
|
try:
|
|
|
|
|
return strip_PKCS7_padding(data)
|
|
|
|
|
except InvalidPadding:
|
|
|
|
|
raise InvalidPassword()
|
|
|
|
|
|
|
|
|
|
|
2018-11-10 15:30:41 +01:00
|
|
|
def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes:
|
2018-11-10 13:30:34 +01:00
|
|
|
assert_bytes(msg)
|
2018-05-24 18:57:13 +02:00
|
|
|
iv = bytes(os.urandom(16))
|
2018-11-10 13:30:34 +01:00
|
|
|
ct = aes_encrypt_with_iv(secret, iv, msg)
|
2018-11-10 15:30:41 +01:00
|
|
|
return iv + ct
|
|
|
|
|
|
2018-05-24 18:57:13 +02:00
|
|
|
|
2018-11-10 15:30:41 +01:00
|
|
|
def DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes:
|
|
|
|
|
assert_bytes(ciphertext)
|
|
|
|
|
iv, e = ciphertext[:16], ciphertext[16:]
|
2018-05-24 18:57:13 +02:00
|
|
|
s = aes_decrypt_with_iv(secret, iv, e)
|
|
|
|
|
return s
|
|
|
|
|
|
2018-11-10 13:30:34 +01:00
|
|
|
|
2018-12-18 15:37:29 +01:00
|
|
|
PW_HASH_VERSION_LATEST = 1
|
2021-03-21 00:34:25 -04:00
|
|
|
KNOWN_PW_HASH_VERSIONS = (1, 2,)
|
|
|
|
|
SUPPORTED_PW_HASH_VERSIONS = (1,)
|
2018-11-10 15:30:41 +01:00
|
|
|
assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS
|
2018-12-18 15:37:29 +01:00
|
|
|
assert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS
|
2018-11-10 15:30:41 +01:00
|
|
|
|
|
|
|
|
|
2018-12-18 19:57:58 +01:00
|
|
|
class UnexpectedPasswordHashVersion(InvalidPassword, WalletFileException):
|
2018-11-10 15:30:41 +01:00
|
|
|
def __init__(self, version):
|
2024-01-17 14:42:19 +00:00
|
|
|
InvalidPassword.__init__(self)
|
|
|
|
|
WalletFileException.__init__(self)
|
2018-11-10 15:30:41 +01:00
|
|
|
self.version = version
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2018-12-18 15:37:29 +01:00
|
|
|
return "{unexpected}: {version}\n{instruction}".format(
|
2018-11-10 15:30:41 +01:00
|
|
|
unexpected=_("Unexpected password hash version"),
|
|
|
|
|
version=self.version,
|
2018-12-18 15:37:29 +01:00
|
|
|
instruction=_('You are most likely using an outdated version of Electrum. Please update.'))
|
2018-11-10 15:30:41 +01:00
|
|
|
|
|
|
|
|
|
2018-12-18 19:57:58 +01:00
|
|
|
class UnsupportedPasswordHashVersion(InvalidPassword, WalletFileException):
|
2018-12-18 15:37:29 +01:00
|
|
|
def __init__(self, version):
|
2024-01-17 14:42:19 +00:00
|
|
|
InvalidPassword.__init__(self)
|
|
|
|
|
WalletFileException.__init__(self)
|
2018-12-18 15:37:29 +01:00
|
|
|
self.version = version
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return "{unsupported}: {version}\n{instruction}".format(
|
|
|
|
|
unsupported=_("Unsupported password hash version"),
|
|
|
|
|
version=self.version,
|
|
|
|
|
instruction=f"To open this wallet, try 'git checkout password_v{self.version}'.\n"
|
|
|
|
|
"Alternatively, restore from seed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _hash_password(password: Union[bytes, str], *, version: int) -> bytes:
|
2018-11-10 15:30:41 +01:00
|
|
|
pw = to_bytes(password, 'utf8')
|
2018-12-18 15:37:29 +01:00
|
|
|
if version not in SUPPORTED_PW_HASH_VERSIONS:
|
|
|
|
|
raise UnsupportedPasswordHashVersion(version)
|
2018-11-10 15:30:41 +01:00
|
|
|
if version == 1:
|
|
|
|
|
return sha256d(pw)
|
|
|
|
|
else:
|
|
|
|
|
assert version not in KNOWN_PW_HASH_VERSIONS
|
|
|
|
|
raise UnexpectedPasswordHashVersion(version)
|
|
|
|
|
|
|
|
|
|
|
2020-06-17 17:17:05 +02:00
|
|
|
def _pw_encode_raw(data: bytes, password: Union[bytes, str], *, version: int) -> bytes:
|
2018-11-10 15:30:41 +01:00
|
|
|
if version not in KNOWN_PW_HASH_VERSIONS:
|
|
|
|
|
raise UnexpectedPasswordHashVersion(version)
|
|
|
|
|
# derive key from password
|
2018-12-18 15:37:29 +01:00
|
|
|
secret = _hash_password(password, version=version)
|
2018-11-10 15:30:41 +01:00
|
|
|
# encrypt given data
|
2020-04-03 12:29:55 +02:00
|
|
|
ciphertext = EncodeAES_bytes(secret, data)
|
2020-06-16 10:42:47 +02:00
|
|
|
return ciphertext
|
2018-11-10 13:30:34 +01:00
|
|
|
|
2020-04-08 12:38:38 +02:00
|
|
|
|
2020-06-17 17:17:05 +02:00
|
|
|
def _pw_decode_raw(data_bytes: bytes, password: Union[bytes, str], *, version: int) -> bytes:
|
2018-11-10 15:30:41 +01:00
|
|
|
if version not in KNOWN_PW_HASH_VERSIONS:
|
|
|
|
|
raise UnexpectedPasswordHashVersion(version)
|
|
|
|
|
# derive key from password
|
2018-12-18 15:37:29 +01:00
|
|
|
secret = _hash_password(password, version=version)
|
2018-11-10 15:30:41 +01:00
|
|
|
# decrypt given data
|
2018-11-10 13:30:34 +01:00
|
|
|
try:
|
2020-04-03 12:29:55 +02:00
|
|
|
d = DecodeAES_bytes(secret, data_bytes)
|
2018-11-10 15:30:41 +01:00
|
|
|
except Exception as e:
|
|
|
|
|
raise InvalidPassword() from e
|
2018-11-10 13:30:34 +01:00
|
|
|
return d
|
2018-05-24 18:57:13 +02:00
|
|
|
|
2020-04-08 12:38:38 +02:00
|
|
|
|
2020-06-16 10:42:47 +02:00
|
|
|
def pw_encode_bytes(data: bytes, password: Union[bytes, str], *, version: int) -> str:
|
|
|
|
|
"""plaintext bytes -> base64 ciphertext"""
|
2020-06-17 17:17:05 +02:00
|
|
|
ciphertext = _pw_encode_raw(data, password, version=version)
|
2020-06-16 10:42:47 +02:00
|
|
|
ciphertext_b64 = base64.b64encode(ciphertext)
|
|
|
|
|
return ciphertext_b64.decode('utf8')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pw_decode_bytes(data: str, password: Union[bytes, str], *, version:int) -> bytes:
|
|
|
|
|
"""base64 ciphertext -> plaintext bytes"""
|
|
|
|
|
if version not in KNOWN_PW_HASH_VERSIONS:
|
|
|
|
|
raise UnexpectedPasswordHashVersion(version)
|
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
```
2022-07-12 15:31:03 +02:00
|
|
|
try:
|
|
|
|
|
data_bytes = bytes(base64.b64decode(data, validate=True))
|
|
|
|
|
except binascii.Error as e:
|
|
|
|
|
raise CiphertextFormatError("ciphertext not valid base64") from e
|
2020-06-17 17:17:05 +02:00
|
|
|
return _pw_decode_raw(data_bytes, password, version=version)
|
2020-06-16 10:42:47 +02:00
|
|
|
|
|
|
|
|
|
2020-06-16 11:52:08 +02:00
|
|
|
def pw_encode_with_version_and_mac(data: bytes, password: Union[bytes, str]) -> str:
|
2020-06-16 10:42:47 +02:00
|
|
|
"""plaintext bytes -> base64 ciphertext"""
|
2020-06-16 11:52:08 +02:00
|
|
|
# https://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac
|
|
|
|
|
# Encrypt-and-MAC. The MAC will be used to detect invalid passwords
|
2020-06-16 10:42:47 +02:00
|
|
|
version = PW_HASH_VERSION_LATEST
|
2020-06-16 11:52:08 +02:00
|
|
|
mac = sha256(data)[0:4]
|
2020-06-17 17:17:05 +02:00
|
|
|
ciphertext = _pw_encode_raw(data, password, version=version)
|
2020-06-16 11:52:08 +02:00
|
|
|
ciphertext_b64 = base64.b64encode(bytes([version]) + ciphertext + mac)
|
2020-06-16 10:42:47 +02:00
|
|
|
return ciphertext_b64.decode('utf8')
|
|
|
|
|
|
|
|
|
|
|
2020-06-16 11:52:08 +02:00
|
|
|
def pw_decode_with_version_and_mac(data: str, password: Union[bytes, str]) -> bytes:
|
2020-06-16 10:42:47 +02:00
|
|
|
"""base64 ciphertext -> plaintext bytes"""
|
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
```
2022-07-12 15:31:03 +02:00
|
|
|
try:
|
|
|
|
|
data_bytes = bytes(base64.b64decode(data, validate=True))
|
|
|
|
|
except binascii.Error as e:
|
|
|
|
|
raise CiphertextFormatError("ciphertext not valid base64") from e
|
2020-06-16 10:42:47 +02:00
|
|
|
version = int(data_bytes[0])
|
2020-06-16 11:52:08 +02:00
|
|
|
encrypted = data_bytes[1:-4]
|
|
|
|
|
mac = data_bytes[-4:]
|
2020-06-16 10:42:47 +02:00
|
|
|
if version not in KNOWN_PW_HASH_VERSIONS:
|
|
|
|
|
raise UnexpectedPasswordHashVersion(version)
|
2020-06-17 17:17:05 +02:00
|
|
|
decrypted = _pw_decode_raw(encrypted, password, version=version)
|
2020-06-16 11:52:08 +02:00
|
|
|
if sha256(decrypted)[0:4] != mac:
|
|
|
|
|
raise InvalidPassword()
|
|
|
|
|
return decrypted
|
2020-06-16 10:42:47 +02:00
|
|
|
|
|
|
|
|
|
2020-04-03 12:29:55 +02:00
|
|
|
def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
|
2020-04-08 12:38:38 +02:00
|
|
|
"""plaintext str -> base64 ciphertext"""
|
2020-04-03 12:29:55 +02:00
|
|
|
if not password:
|
|
|
|
|
return data
|
2020-04-08 12:38:38 +02:00
|
|
|
plaintext_bytes = to_bytes(data, "utf8")
|
|
|
|
|
return pw_encode_bytes(plaintext_bytes, password, version=version)
|
|
|
|
|
|
2020-04-03 12:29:55 +02:00
|
|
|
|
|
|
|
|
def pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
|
2020-04-08 12:38:38 +02:00
|
|
|
"""base64 ciphertext -> plaintext str"""
|
2020-04-03 12:29:55 +02:00
|
|
|
if password is None:
|
|
|
|
|
return data
|
2020-04-08 12:38:38 +02:00
|
|
|
plaintext_bytes = pw_decode_bytes(data, password, version=version)
|
2020-04-08 12:49:50 +02:00
|
|
|
try:
|
|
|
|
|
plaintext_str = to_string(plaintext_bytes, "utf8")
|
|
|
|
|
except UnicodeDecodeError as e:
|
|
|
|
|
raise InvalidPassword() from e
|
2020-04-08 12:38:38 +02:00
|
|
|
return plaintext_str
|
2020-04-03 12:29:55 +02:00
|
|
|
|
2018-05-24 18:57:13 +02:00
|
|
|
|
2018-10-25 22:28:24 +02:00
|
|
|
def sha256(x: Union[bytes, str]) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
x = to_bytes(x, 'utf8')
|
|
|
|
|
return bytes(hashlib.sha256(x).digest())
|
|
|
|
|
|
|
|
|
|
|
2018-10-25 22:28:24 +02:00
|
|
|
def sha256d(x: Union[bytes, str]) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
x = to_bytes(x, 'utf8')
|
|
|
|
|
out = bytes(sha256(sha256(x)))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def hash_160(x: bytes) -> bytes:
|
2018-05-26 17:04:55 +02:00
|
|
|
return ripemd(sha256(x))
|
2018-04-17 06:59:21 +02:00
|
|
|
|
2023-11-22 17:50:29 +00:00
|
|
|
def ripemd(x: bytes) -> bytes:
|
2018-05-24 18:57:13 +02:00
|
|
|
try:
|
|
|
|
|
md = hashlib.new('ripemd160')
|
2018-04-17 06:59:21 +02:00
|
|
|
md.update(x)
|
2018-05-24 18:57:13 +02:00
|
|
|
return md.digest()
|
|
|
|
|
except BaseException:
|
2021-03-10 21:28:17 +01:00
|
|
|
# ripemd160 is not guaranteed to be available in hashlib on all platforms.
|
|
|
|
|
# Historically, our Android builds had hashlib/openssl which did not have it.
|
|
|
|
|
# see https://github.com/spesmilo/electrum/issues/7093
|
|
|
|
|
# We bundle a pure python implementation as fallback that gets used now:
|
2018-05-24 18:57:13 +02:00
|
|
|
from . import ripemd
|
2018-04-17 06:59:21 +02:00
|
|
|
md = ripemd.new(x)
|
2018-05-24 18:57:13 +02:00
|
|
|
return md.digest()
|
2018-06-28 11:42:47 +02:00
|
|
|
|
2024-06-05 14:55:48 +00:00
|
|
|
|
2018-06-28 11:42:47 +02:00
|
|
|
def hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes:
|
2024-06-05 14:55:48 +00:00
|
|
|
return hmac.digest(key, msg, digest)
|
2020-03-04 16:15:22 +01:00
|
|
|
|
|
|
|
|
|
2020-06-17 17:32:51 +02:00
|
|
|
def chacha20_poly1305_encrypt(
|
|
|
|
|
*,
|
|
|
|
|
key: bytes,
|
|
|
|
|
nonce: bytes,
|
|
|
|
|
associated_data: bytes = None,
|
|
|
|
|
data: bytes
|
|
|
|
|
) -> bytes:
|
2020-03-04 18:54:20 +01:00
|
|
|
assert isinstance(key, (bytes, bytearray))
|
|
|
|
|
assert isinstance(nonce, (bytes, bytearray))
|
2020-06-17 17:32:51 +02:00
|
|
|
assert isinstance(associated_data, (bytes, bytearray, type(None)))
|
2020-03-04 18:54:20 +01:00
|
|
|
assert isinstance(data, (bytes, bytearray))
|
2021-03-10 17:31:02 +01:00
|
|
|
assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)"
|
2021-03-10 17:13:42 +01:00
|
|
|
assert len(nonce) == 12, f"unexpected nonce size: {len(nonce)} (expected: 12)"
|
2020-03-04 18:54:20 +01:00
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce)
|
2020-06-17 17:32:51 +02:00
|
|
|
if associated_data is not None:
|
|
|
|
|
cipher.update(associated_data)
|
2020-03-04 18:54:20 +01:00
|
|
|
ciphertext, mac = cipher.encrypt_and_digest(plaintext=data)
|
|
|
|
|
return ciphertext + mac
|
|
|
|
|
if HAS_CRYPTOGRAPHY:
|
|
|
|
|
a = CG_aead.ChaCha20Poly1305(key)
|
|
|
|
|
return a.encrypt(nonce, data, associated_data)
|
2020-05-03 03:08:28 +00:00
|
|
|
raise Exception("no chacha20 backend found")
|
2020-03-04 16:15:22 +01:00
|
|
|
|
|
|
|
|
|
2020-06-17 17:32:51 +02:00
|
|
|
def chacha20_poly1305_decrypt(
|
|
|
|
|
*,
|
|
|
|
|
key: bytes,
|
|
|
|
|
nonce: bytes,
|
|
|
|
|
associated_data: bytes = None,
|
|
|
|
|
data: bytes
|
|
|
|
|
) -> bytes:
|
2020-03-04 18:54:20 +01:00
|
|
|
assert isinstance(key, (bytes, bytearray))
|
|
|
|
|
assert isinstance(nonce, (bytes, bytearray))
|
2020-06-17 17:32:51 +02:00
|
|
|
assert isinstance(associated_data, (bytes, bytearray, type(None)))
|
2020-03-04 18:54:20 +01:00
|
|
|
assert isinstance(data, (bytes, bytearray))
|
2021-03-10 17:31:02 +01:00
|
|
|
assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)"
|
2021-03-10 17:13:42 +01:00
|
|
|
assert len(nonce) == 12, f"unexpected nonce size: {len(nonce)} (expected: 12)"
|
2020-03-04 18:54:20 +01:00
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce)
|
2020-06-17 17:32:51 +02:00
|
|
|
if associated_data is not None:
|
|
|
|
|
cipher.update(associated_data)
|
2020-03-04 18:54:20 +01:00
|
|
|
# raises ValueError if not valid (e.g. incorrect MAC)
|
|
|
|
|
return cipher.decrypt_and_verify(ciphertext=data[:-16], received_mac_tag=data[-16:])
|
|
|
|
|
if HAS_CRYPTOGRAPHY:
|
|
|
|
|
a = CG_aead.ChaCha20Poly1305(key)
|
|
|
|
|
try:
|
|
|
|
|
return a.decrypt(nonce, data, associated_data)
|
|
|
|
|
except cryptography.exceptions.InvalidTag as e:
|
|
|
|
|
raise ValueError("invalid tag") from e
|
2020-05-03 03:08:28 +00:00
|
|
|
raise Exception("no chacha20 backend found")
|
2020-03-04 16:15:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def chacha20_encrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes:
|
2024-05-22 13:39:27 +00:00
|
|
|
"""note: for any new protocol you design, please consider using chacha20_poly1305_encrypt instead
|
|
|
|
|
(for its Authenticated Encryption property).
|
|
|
|
|
"""
|
2020-03-04 18:54:20 +01:00
|
|
|
assert isinstance(key, (bytes, bytearray))
|
|
|
|
|
assert isinstance(nonce, (bytes, bytearray))
|
|
|
|
|
assert isinstance(data, (bytes, bytearray))
|
2021-03-10 17:31:02 +01:00
|
|
|
assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)"
|
2021-03-10 17:13:42 +01:00
|
|
|
assert len(nonce) in (8, 12), f"unexpected nonce size: {len(nonce)} (expected: 8 or 12)"
|
2020-03-04 18:54:20 +01:00
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
cipher = CD_ChaCha20.new(key=key, nonce=nonce)
|
|
|
|
|
return cipher.encrypt(data)
|
|
|
|
|
if HAS_CRYPTOGRAPHY:
|
2021-03-10 17:13:42 +01:00
|
|
|
nonce = bytes(16 - len(nonce)) + nonce # cryptography wants 16 byte nonces
|
2020-03-04 18:54:20 +01:00
|
|
|
algo = CG_algorithms.ChaCha20(key=key, nonce=nonce)
|
|
|
|
|
cipher = CG_Cipher(algo, mode=None, backend=CG_default_backend())
|
|
|
|
|
encryptor = cipher.encryptor()
|
|
|
|
|
return encryptor.update(data)
|
2020-05-03 03:08:28 +00:00
|
|
|
raise Exception("no chacha20 backend found")
|
2021-03-10 17:13:42 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def chacha20_decrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes:
|
|
|
|
|
assert isinstance(key, (bytes, bytearray))
|
|
|
|
|
assert isinstance(nonce, (bytes, bytearray))
|
|
|
|
|
assert isinstance(data, (bytes, bytearray))
|
2021-03-10 17:31:02 +01:00
|
|
|
assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)"
|
2021-03-10 17:13:42 +01:00
|
|
|
assert len(nonce) in (8, 12), f"unexpected nonce size: {len(nonce)} (expected: 8 or 12)"
|
|
|
|
|
if HAS_CRYPTODOME:
|
|
|
|
|
cipher = CD_ChaCha20.new(key=key, nonce=nonce)
|
|
|
|
|
return cipher.decrypt(data)
|
|
|
|
|
if HAS_CRYPTOGRAPHY:
|
|
|
|
|
nonce = bytes(16 - len(nonce)) + nonce # cryptography wants 16 byte nonces
|
|
|
|
|
algo = CG_algorithms.ChaCha20(key=key, nonce=nonce)
|
|
|
|
|
cipher = CG_Cipher(algo, mode=None, backend=CG_default_backend())
|
|
|
|
|
decryptor = cipher.decryptor()
|
|
|
|
|
return decryptor.update(data)
|
|
|
|
|
raise Exception("no chacha20 backend found")
|
2024-06-17 11:20:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def ecies_encrypt_message(ec_pubkey, message: bytes, *, magic: bytes = b'BIE1') -> bytes:
|
|
|
|
|
"""
|
|
|
|
|
ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
|
|
|
|
|
"""
|
|
|
|
|
assert_bytes(message)
|
|
|
|
|
ephemeral = ecc.ECPrivkey.generate_random_key()
|
|
|
|
|
ecdh_key = (ec_pubkey * ephemeral.secret_scalar).get_public_key_bytes(compressed=True)
|
|
|
|
|
key = hashlib.sha512(ecdh_key).digest()
|
|
|
|
|
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
|
|
|
|
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
|
|
|
|
|
ephemeral_pubkey = ephemeral.get_public_key_bytes(compressed=True)
|
|
|
|
|
encrypted = magic + ephemeral_pubkey + ciphertext
|
|
|
|
|
mac = hmac_oneshot(key_m, encrypted, hashlib.sha256)
|
|
|
|
|
return base64.b64encode(encrypted + mac)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ecies_decrypt_message(ec_privkey, encrypted: Union[str, bytes], *, magic: bytes=b'BIE1') -> bytes:
|
|
|
|
|
encrypted = base64.b64decode(encrypted) # type: bytes
|
|
|
|
|
if len(encrypted) < 85:
|
|
|
|
|
raise Exception('invalid ciphertext: length')
|
|
|
|
|
magic_found = encrypted[:4]
|
|
|
|
|
ephemeral_pubkey_bytes = encrypted[4:37]
|
|
|
|
|
ciphertext = encrypted[37:-32]
|
|
|
|
|
mac = encrypted[-32:]
|
|
|
|
|
if magic_found != magic:
|
|
|
|
|
raise Exception('invalid ciphertext: invalid magic bytes')
|
|
|
|
|
try:
|
|
|
|
|
ephemeral_pubkey = ecc.ECPubkey(ephemeral_pubkey_bytes)
|
|
|
|
|
except ecc.InvalidECPointException as e:
|
|
|
|
|
raise Exception('invalid ciphertext: invalid ephemeral pubkey') from e
|
|
|
|
|
ecdh_key = (ephemeral_pubkey * ec_privkey.secret_scalar).get_public_key_bytes(compressed=True)
|
|
|
|
|
key = hashlib.sha512(ecdh_key).digest()
|
|
|
|
|
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
|
|
|
|
if mac != hmac_oneshot(key_m, encrypted[:-32], hashlib.sha256):
|
|
|
|
|
raise InvalidPassword()
|
|
|
|
|
return aes_decrypt_with_iv(key_e, iv, ciphertext)
|