Browse Source

Merge pull request #6014 from SomberNight/20200304_pycryptodomex

add 'cryptography' as optional dependency; clean README and sdist
hard-fail-on-bad-server-string
ThomasV 5 years ago
committed by GitHub
parent
commit
dbd77b7d8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .travis.yml
  2. 41
      README.rst
  3. 33
      contrib/deterministic-build/requirements-binaries.txt
  4. 33
      contrib/deterministic-build/requirements.txt
  5. 1
      contrib/requirements/requirements-binaries.txt
  6. 1
      contrib/requirements/requirements.txt
  7. 96
      electrum/crypto.py
  8. 9
      electrum/lnonion.py
  9. 20
      electrum/lntransport.py
  10. 84
      electrum/tests/test_bitcoin.py
  11. 4
      electrum/tests/test_lnpeer.py
  12. 5
      electrum/tests/test_lnrouter.py
  13. 3
      electrum/tests/test_lntransport.py
  14. 7
      setup.py
  15. 2
      tox.ini

2
.travis.yml

@ -30,7 +30,7 @@ jobs:
- sudo apt-get -qq update - sudo apt-get -qq update
- sudo apt-get install -yq bitcoind - sudo apt-get install -yq bitcoind
- sudo apt-get -y install libsecp256k1-0 - sudo apt-get -y install libsecp256k1-0
- pip install -r contrib/requirements/requirements.txt - pip install .[tests]
- pip install electrumx - pip install electrumx
before_script: before_script:
- electrum/tests/regtest/start_bitcoind.sh - electrum/tests/regtest/start_bitcoind.sh

41
README.rst

@ -26,16 +26,26 @@ Electrum - Lightweight Bitcoin client
Getting started Getting started
=============== ===============
Electrum itself is pure Python, and so are most of the required dependencies. (*If you've come here looking to simply run Electrum,* `you may download it here`_.)
Non-python dependencies .. _you may download it here: https://electrum.org/#download
-----------------------
Electrum itself is pure Python, and so are most of the required dependencies,
but not everything. The following sections describe how to run from source, but here
is a TL;DR::
sudo apt-get install libsecp256k1-0
python3 -m pip install --user .[gui,crypto]
Not pure-python dependencies
----------------------------
If you want to use the Qt interface, install the Qt dependencies:: If you want to use the Qt interface, install the Qt dependencies::
sudo apt-get install python3-pyqt5 sudo apt-get install python3-pyqt5
For elliptic curve operations, libsecp256k1 is a required dependency:: For elliptic curve operations, `libsecp256k1`_ is a required dependency::
sudo apt-get install libsecp256k1-0 sudo apt-get install libsecp256k1-0
@ -44,13 +54,26 @@ libsecp256k1 yourself::
./contrib/make_libsecp256k1.sh ./contrib/make_libsecp256k1.sh
Due to the need for fast symmetric ciphers, either one of `pycryptodomex`_
or `cryptography`_ is required. Install from your package manager
(or from pip)::
sudo apt-get install python3-cryptography
If you would like hardware wallet support, see `this`_.
.. _libsecp256k1: https://github.com/bitcoin-core/secp256k1
.. _pycryptodomex: https://github.com/Legrandin/pycryptodome
.. _cryptography: https://github.com/pyca/cryptography
.. _this: https://github.com/spesmilo/electrum-docs/blob/master/hardware-linux.rst
Running from tar.gz Running from tar.gz
------------------- -------------------
If you downloaded the official package (tar.gz), you can run If you downloaded the official package (tar.gz), you can run
Electrum from its root directory without installing it on your Electrum from its root directory without installing it on your
system; all the python dependencies are included in the 'packages' system; all the pure python dependencies are included in the 'packages'
directory. To run Electrum from its root directory, just do:: directory. To run Electrum from its root directory, just do::
./run_electrum ./run_electrum
@ -63,13 +86,9 @@ You can also install Electrum on your system, by running this command::
This will download and install the Python dependencies used by This will download and install the Python dependencies used by
Electrum instead of using the 'packages' directory. Electrum instead of using the 'packages' directory.
If you cloned the git repository, you need to compile extra files
before you can run Electrum. Read the next section, "Development
version".
Development version (git clone)
Development version -------------------------------
-------------------
Check out the code from GitHub:: Check out the code from GitHub::

33
contrib/deterministic-build/requirements-binaries.txt

@ -1,6 +1,39 @@
pip==19.3.1 \ pip==19.3.1 \
--hash=sha256:21207d76c1031e517668898a6b46a9fb1501c7a4710ef5dfd6a40ad9e6757ea7 \ --hash=sha256:21207d76c1031e517668898a6b46a9fb1501c7a4710ef5dfd6a40ad9e6757ea7 \
--hash=sha256:6917c65fc3769ecdc61405d3dfd97afdedd75808d200b2838d7d961cebc0c2c7 --hash=sha256:6917c65fc3769ecdc61405d3dfd97afdedd75808d200b2838d7d961cebc0c2c7
pycryptodomex==3.9.4 \
--hash=sha256:0943b65fb41b7403a9def6214061fdd9ab9afd0bbc581e553c72eebe60bded36 \
--hash=sha256:0a1dbb5c4d975a4ea568fb7686550aa225d94023191fb0cca8747dc5b5d77857 \
--hash=sha256:0f43f1608518347fdcb9c8f443fa5cabedd33f94188b13e4196a3a7ba90d169c \
--hash=sha256:11ce5fec5990e34e3981ed14897ba601c83957b577d77d395f1f8f878a179f98 \
--hash=sha256:17a09e38fdc91e4857cf5a7ce82f3c0b229c3977490f2146513e366923fc256b \
--hash=sha256:22d970cee5c096b9123415e183ae03702b2cd4d3ba3f0ced25c4e1aba3967167 \
--hash=sha256:2a1793efcbae3a2264c5e0e492a2629eb10d895d6e5f17dbbd00eb8b489c6bda \
--hash=sha256:30a8a148a0fe482cec1aaf942bbd0ade56ec197c14fe058b2a94318c57e1f991 \
--hash=sha256:32fbbaf964c5184d3f3e349085b0536dd28184b02e2b014fc900f58bbc126339 \
--hash=sha256:347d67faee36d449dc9632da411cc318df52959079062627f1243001b10dc227 \
--hash=sha256:45f4b4e5461a041518baabc52340c249b60833aa84cea6377dc8016a2b33c666 \
--hash=sha256:4717daec0035034b002d31c42e55431c970e3e38a78211f43990e1b7eaf19e28 \
--hash=sha256:51a1ac9e7dda81da444fed8be558a60ec88dfc73b2aa4b0efa310e87acb75838 \
--hash=sha256:53e9dcc8f14783f6300b70da325a50ac1b0a3dbaee323bd9dc3f71d409c197a1 \
--hash=sha256:5519a2ed776e193688b7ddb61ab709303f6eb7d1237081e298283c72acc44271 \
--hash=sha256:583450e8e80a0885c453211ed2bd69ceea634d8c904f23ff8687f677fe810e95 \
--hash=sha256:60f862bd2a07133585a4fc2ce2b1a8ec24746b07ac44307d22ef2b767cb03435 \
--hash=sha256:612091f1d3c84e723bec7cb855cf77576e646045744794c9a3f75ba80737762f \
--hash=sha256:629a87b87c8203b8789ccefc7f2f2faecd2daaeb56bdd0b4e44cd89565f2db07 \
--hash=sha256:6e56ec4c8938fb388b6f250ddd5e21c15e8f25a76e0ad0e2abae9afee09e67b4 \
--hash=sha256:8e8092651844a11ec7fa534395f3dfe99256ce4edca06f128efc9d770d6e1dc1 \
--hash=sha256:8f5f260629876603e08f3ce95c8ccd9b6b83bf9a921c41409046796267f7adc5 \
--hash=sha256:9a6b74f38613f54c56bd759b411a352258f47489bbefd1d57c930a291498b35b \
--hash=sha256:a5a13ebb52c4cd065fb673d8c94f39f30823428a4de19e1f3f828b63a8882d1e \
--hash=sha256:a77ca778a476829876a3a70ae880073379160e4a465d057e3c4e1c79acdf1b8a \
--hash=sha256:a9f7be3d19f79429c2118fd61bc2ec4fa095e93b56fb3a5f3009822402c4380f \
--hash=sha256:dc15a467c4f9e4b43748ba2f97aea66f67812bfd581818284c47cadc81d4caec \
--hash=sha256:e13cdeea23059f7577c230fd580d2c8178e67ebe10e360041abe86c33c316f1c \
--hash=sha256:e45b85c8521bca6bdfaf57e4987743ade53e9f03529dd3adbc9524094c6d55c4 \
--hash=sha256:e87f17867b260f57c88487f943eb4d46c90532652bb37046e764842c3b66cbb1 \
--hash=sha256:ee40a5b156f6c1192bc3082e9d73d0479904433cdda83110546cd67f5a15a5be \
--hash=sha256:ef63ffde3b267043579af8830fc97fc3b9b8a526a24e3ba23af9989d4e9e689a
PyQt5==5.11.3 \ PyQt5==5.11.3 \
--hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \ --hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \
--hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \ --hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \

33
contrib/deterministic-build/requirements.txt

@ -103,39 +103,6 @@ protobuf==3.11.1 \
--hash=sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13 --hash=sha256:e88a924b591b06d0191620e9c8aa75297b3111066bb09d49a24bae1054a10c13
pyaes==1.6.1 \ pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
pycryptodomex==3.9.4 \
--hash=sha256:0943b65fb41b7403a9def6214061fdd9ab9afd0bbc581e553c72eebe60bded36 \
--hash=sha256:0a1dbb5c4d975a4ea568fb7686550aa225d94023191fb0cca8747dc5b5d77857 \
--hash=sha256:0f43f1608518347fdcb9c8f443fa5cabedd33f94188b13e4196a3a7ba90d169c \
--hash=sha256:11ce5fec5990e34e3981ed14897ba601c83957b577d77d395f1f8f878a179f98 \
--hash=sha256:17a09e38fdc91e4857cf5a7ce82f3c0b229c3977490f2146513e366923fc256b \
--hash=sha256:22d970cee5c096b9123415e183ae03702b2cd4d3ba3f0ced25c4e1aba3967167 \
--hash=sha256:2a1793efcbae3a2264c5e0e492a2629eb10d895d6e5f17dbbd00eb8b489c6bda \
--hash=sha256:30a8a148a0fe482cec1aaf942bbd0ade56ec197c14fe058b2a94318c57e1f991 \
--hash=sha256:32fbbaf964c5184d3f3e349085b0536dd28184b02e2b014fc900f58bbc126339 \
--hash=sha256:347d67faee36d449dc9632da411cc318df52959079062627f1243001b10dc227 \
--hash=sha256:45f4b4e5461a041518baabc52340c249b60833aa84cea6377dc8016a2b33c666 \
--hash=sha256:4717daec0035034b002d31c42e55431c970e3e38a78211f43990e1b7eaf19e28 \
--hash=sha256:51a1ac9e7dda81da444fed8be558a60ec88dfc73b2aa4b0efa310e87acb75838 \
--hash=sha256:53e9dcc8f14783f6300b70da325a50ac1b0a3dbaee323bd9dc3f71d409c197a1 \
--hash=sha256:5519a2ed776e193688b7ddb61ab709303f6eb7d1237081e298283c72acc44271 \
--hash=sha256:583450e8e80a0885c453211ed2bd69ceea634d8c904f23ff8687f677fe810e95 \
--hash=sha256:60f862bd2a07133585a4fc2ce2b1a8ec24746b07ac44307d22ef2b767cb03435 \
--hash=sha256:612091f1d3c84e723bec7cb855cf77576e646045744794c9a3f75ba80737762f \
--hash=sha256:629a87b87c8203b8789ccefc7f2f2faecd2daaeb56bdd0b4e44cd89565f2db07 \
--hash=sha256:6e56ec4c8938fb388b6f250ddd5e21c15e8f25a76e0ad0e2abae9afee09e67b4 \
--hash=sha256:8e8092651844a11ec7fa534395f3dfe99256ce4edca06f128efc9d770d6e1dc1 \
--hash=sha256:8f5f260629876603e08f3ce95c8ccd9b6b83bf9a921c41409046796267f7adc5 \
--hash=sha256:9a6b74f38613f54c56bd759b411a352258f47489bbefd1d57c930a291498b35b \
--hash=sha256:a5a13ebb52c4cd065fb673d8c94f39f30823428a4de19e1f3f828b63a8882d1e \
--hash=sha256:a77ca778a476829876a3a70ae880073379160e4a465d057e3c4e1c79acdf1b8a \
--hash=sha256:a9f7be3d19f79429c2118fd61bc2ec4fa095e93b56fb3a5f3009822402c4380f \
--hash=sha256:dc15a467c4f9e4b43748ba2f97aea66f67812bfd581818284c47cadc81d4caec \
--hash=sha256:e13cdeea23059f7577c230fd580d2c8178e67ebe10e360041abe86c33c316f1c \
--hash=sha256:e45b85c8521bca6bdfaf57e4987743ade53e9f03529dd3adbc9524094c6d55c4 \
--hash=sha256:e87f17867b260f57c88487f943eb4d46c90532652bb37046e764842c3b66cbb1 \
--hash=sha256:ee40a5b156f6c1192bc3082e9d73d0479904433cdda83110546cd67f5a15a5be \
--hash=sha256:ef63ffde3b267043579af8830fc97fc3b9b8a526a24e3ba23af9989d4e9e689a
pyrsistent==0.15.6 \ pyrsistent==0.15.6 \
--hash=sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b --hash=sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b
QDarkStyle==2.6.8 \ QDarkStyle==2.6.8 \

1
contrib/requirements/requirements-binaries.txt

@ -1,2 +1,3 @@
PyQt5<5.12 PyQt5<5.12
PyQt5-sip<=4.19.13 PyQt5-sip<=4.19.13
pycryptodomex>=3.7

1
contrib/requirements/requirements.txt

@ -9,7 +9,6 @@ aiohttp>=3.3.0,<4.0.0
aiohttp_socks aiohttp_socks
certifi certifi
bitstring bitstring
pycryptodomex>=3.7
jsonrpcserver jsonrpcserver
jsonrpcclient jsonrpcclient
attrs attrs

96
electrum/crypto.py

@ -25,6 +25,7 @@
import base64 import base64
import os import os
import sys
import hashlib import hashlib
import hmac import hmac
from typing import Union from typing import Union
@ -35,10 +36,33 @@ from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFile
from .i18n import _ from .i18n import _
HAS_CRYPTODOME = False
try: try:
from Cryptodome.Cipher import AES 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
except: except:
AES = None pass
else:
HAS_CRYPTODOME = True
HAS_CRYPTOGRAPHY = False
try:
import cryptography
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
except:
pass
else:
HAS_CRYPTOGRAPHY = True
if not (HAS_CRYPTODOME or HAS_CRYPTOGRAPHY):
sys.exit(f"Error: at least one of ('pycryptodomex', 'cryptography') needs to be installed.")
class InvalidPadding(Exception): class InvalidPadding(Exception):
@ -67,8 +91,12 @@ def strip_PKCS7_padding(data: bytes) -> bytes:
def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
assert_bytes(key, iv, data) assert_bytes(key, iv, data)
data = append_PKCS7_padding(data) data = append_PKCS7_padding(data)
if AES: if HAS_CRYPTODOME:
e = AES.new(key, AES.MODE_CBC, iv).encrypt(data) 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()
else: else:
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE) aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE)
@ -78,9 +106,13 @@ def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
assert_bytes(key, iv, data) assert_bytes(key, iv, data)
if AES: if HAS_CRYPTODOME:
cipher = AES.new(key, AES.MODE_CBC, iv) cipher = CD_AES.new(key, CD_AES.MODE_CBC, iv)
data = cipher.decrypt(data) data = cipher.decrypt(data)
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()
else: else:
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE) aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE)
@ -216,3 +248,55 @@ def hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes:
return hmac.digest(key, msg, digest) return hmac.digest(key, msg, digest)
else: else:
return hmac.new(key, msg, digest).digest() return hmac.new(key, msg, digest).digest()
def chacha20_poly1305_encrypt(*, key: bytes, nonce: bytes, associated_data: bytes, data: bytes) -> bytes:
assert isinstance(key, (bytes, bytearray))
assert isinstance(nonce, (bytes, bytearray))
assert isinstance(associated_data, (bytes, bytearray))
assert isinstance(data, (bytes, bytearray))
if HAS_CRYPTODOME:
cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce)
cipher.update(associated_data)
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)
raise Exception("no chacha20 backed found")
def chacha20_poly1305_decrypt(*, key: bytes, nonce: bytes, associated_data: bytes, data: bytes) -> bytes:
assert isinstance(key, (bytes, bytearray))
assert isinstance(nonce, (bytes, bytearray))
assert isinstance(associated_data, (bytes, bytearray))
assert isinstance(data, (bytes, bytearray))
if HAS_CRYPTODOME:
cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce)
cipher.update(associated_data)
# 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
raise Exception("no chacha20 backed found")
def chacha20_encrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes:
assert isinstance(key, (bytes, bytearray))
assert isinstance(nonce, (bytes, bytearray))
assert isinstance(data, (bytes, bytearray))
assert len(nonce) == 8, f"unexpected nonce size: {len(nonce)} (expected: 8)"
if HAS_CRYPTODOME:
cipher = CD_ChaCha20.new(key=key, nonce=nonce)
return cipher.encrypt(data)
if HAS_CRYPTOGRAPHY:
nonce = bytes(8) + nonce # cryptography wants 16 byte nonces
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)
raise Exception("no chacha20 backed found")

9
electrum/lnonion.py

@ -27,10 +27,8 @@ import hashlib
from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from Cryptodome.Cipher import ChaCha20
from . import ecc from . import ecc
from .crypto import sha256, hmac_oneshot from .crypto import sha256, hmac_oneshot, chacha20_encrypt
from .util import bh2u, profiler, xor_bytes, bfh from .util import bh2u, profiler, xor_bytes, bfh
from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH, from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID) NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID)
@ -227,8 +225,9 @@ def generate_filler(key_type: bytes, num_hops: int, hop_size: int,
def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:
cipher = ChaCha20.new(key=stream_key, nonce=bytes(8)) return chacha20_encrypt(key=stream_key,
return cipher.encrypt(bytes(num_bytes)) nonce=bytes(8),
data=bytes(num_bytes))
class ProcessedOnionPacket(NamedTuple): class ProcessedOnionPacket(NamedTuple):

20
electrum/lntransport.py

@ -9,9 +9,7 @@ import hashlib
import asyncio import asyncio
from asyncio import StreamReader, StreamWriter from asyncio import StreamReader, StreamWriter
from Cryptodome.Cipher import ChaCha20_Poly1305 from .crypto import sha256, hmac_oneshot, chacha20_poly1305_encrypt, chacha20_poly1305_decrypt
from .crypto import sha256, hmac_oneshot
from .lnutil import (get_ecdh, privkey_to_pubkey, LightningPeerConnectionClosed, from .lnutil import (get_ecdh, privkey_to_pubkey, LightningPeerConnectionClosed,
HandshakeFailed, LNPeerAddr) HandshakeFailed, LNPeerAddr)
from . import ecc from . import ecc
@ -41,17 +39,17 @@ def get_nonce_bytes(n):
def aead_encrypt(key: bytes, nonce: int, associated_data: bytes, data: bytes) -> bytes: def aead_encrypt(key: bytes, nonce: int, associated_data: bytes, data: bytes) -> bytes:
nonce_bytes = get_nonce_bytes(nonce) nonce_bytes = get_nonce_bytes(nonce)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce_bytes) return chacha20_poly1305_encrypt(key=key,
cipher.update(associated_data) nonce=nonce_bytes,
ciphertext, mac = cipher.encrypt_and_digest(plaintext=data) associated_data=associated_data,
return ciphertext + mac data=data)
def aead_decrypt(key: bytes, nonce: int, associated_data: bytes, data: bytes) -> bytes: def aead_decrypt(key: bytes, nonce: int, associated_data: bytes, data: bytes) -> bytes:
nonce_bytes = get_nonce_bytes(nonce) nonce_bytes = get_nonce_bytes(nonce)
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce_bytes) return chacha20_poly1305_decrypt(key=key,
cipher.update(associated_data) nonce=nonce_bytes,
# raises ValueError if not valid (e.g. incorrect MAC) associated_data=associated_data,
return cipher.decrypt_and_verify(ciphertext=data[:-16], received_mac_tag=data[-16:]) data=data)
def get_bolt8_hkdf(salt, ikm): def get_bolt8_hkdf(salt, ikm):
"""RFC5869 HKDF instantiated in the specific form """RFC5869 HKDF instantiated in the specific form

84
electrum/tests/test_bitcoin.py

@ -34,8 +34,8 @@ except ImportError:
def needs_test_with_all_aes_implementations(func): def needs_test_with_all_aes_implementations(func):
"""Function decorator to run a unit test twice: """Function decorator to run a unit test multiple times:
once when pycryptodomex is not available, once when it is. once with each AES implementation.
NOTE: this is inherently sequential; NOTE: this is inherently sequential;
tests running in parallel would break things tests running in parallel would break things
@ -44,18 +44,46 @@ def needs_test_with_all_aes_implementations(func):
if FAST_TESTS: # if set, only run tests once, using fastest implementation if FAST_TESTS: # if set, only run tests once, using fastest implementation
func(*args, **kwargs) func(*args, **kwargs)
return return
_aes = crypto.AES has_cryptodome = crypto.HAS_CRYPTODOME
crypto.AES = None has_cryptography = crypto.HAS_CRYPTOGRAPHY
try: try:
# first test without pycryptodomex (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, False
func(*args, **kwargs) func(*args, **kwargs) # pyaes
if has_cryptodome:
(crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False
func(*args, **kwargs) # cryptodome
if has_cryptography:
(crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True
func(*args, **kwargs) # cryptography
finally: finally:
crypto.AES = _aes crypto.HAS_CRYPTODOME = has_cryptodome
# if pycryptodomex is not available, we are done crypto.HAS_CRYPTOGRAPHY = has_cryptography
if not _aes: return run_test
return
# if pycryptodomex is available, test again now
def needs_test_with_all_chacha20_implementations(func):
"""Function decorator to run a unit test multiple times:
once with each ChaCha20/Poly1305 implementation.
NOTE: this is inherently sequential;
tests running in parallel would break things
"""
def run_test(*args, **kwargs):
if FAST_TESTS: # if set, only run tests once, using fastest implementation
func(*args, **kwargs) func(*args, **kwargs)
return
has_cryptodome = crypto.HAS_CRYPTODOME
has_cryptography = crypto.HAS_CRYPTOGRAPHY
try:
if has_cryptodome:
(crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False
func(*args, **kwargs) # cryptodome
if has_cryptography:
(crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True
func(*args, **kwargs) # cryptography
finally:
crypto.HAS_CRYPTODOME = has_cryptodome
crypto.HAS_CRYPTOGRAPHY = has_cryptography
return run_test return run_test
@ -67,7 +95,11 @@ class Test_bitcoin(ElectrumTestCase):
def test_pycryptodomex_is_available(self): def test_pycryptodomex_is_available(self):
# we want the unit testing framework to test with pycryptodomex available. # we want the unit testing framework to test with pycryptodomex available.
self.assertTrue(bool(crypto.AES)) self.assertTrue(bool(crypto.HAS_CRYPTODOME))
def test_cryptography_is_available(self):
# we want the unit testing framework to test with cryptography available.
self.assertTrue(bool(crypto.HAS_CRYPTOGRAPHY))
@needs_test_with_all_aes_implementations @needs_test_with_all_aes_implementations
def test_crypto(self): def test_crypto(self):
@ -223,6 +255,34 @@ class Test_bitcoin(ElectrumTestCase):
with self.assertRaises(InvalidPassword): with self.assertRaises(InvalidPassword):
crypto.pw_decode(enc, wrong_password, version=version) crypto.pw_decode(enc, wrong_password, version=version)
@needs_test_with_all_chacha20_implementations
def test_chacha20_poly1305_encrypt(self):
key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')
nonce = bytes.fromhex('010203040506070809101112')
associated_data = bytes.fromhex('30c9572d4305d4f3ccb766b1db884da6f1e0086f55136a39740700c272095717')
data = bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328')
self.assertEqual(bytes.fromhex('90fb51fcde1fbe4013500bd7a32280445d80ee21f0aa3acd30df72cf609de064'),
crypto.chacha20_poly1305_encrypt(key=key, nonce=nonce, associated_data=associated_data, data=data))
@needs_test_with_all_chacha20_implementations
def test_chacha20_poly1305_decrypt(self):
key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')
nonce = bytes.fromhex('010203040506070809101112')
associated_data = bytes.fromhex('30c9572d4305d4f3ccb766b1db884da6f1e0086f55136a39740700c272095717')
data = bytes.fromhex('90fb51fcde1fbe4013500bd7a32280445d80ee21f0aa3acd30df72cf609de064')
self.assertEqual(bytes.fromhex('4a6cd75da76cedf0a8a47e3a5734a328'),
crypto.chacha20_poly1305_decrypt(key=key, nonce=nonce, associated_data=associated_data, data=data))
with self.assertRaises(ValueError):
crypto.chacha20_poly1305_decrypt(key=key, nonce=nonce, associated_data=b'', data=data)
@needs_test_with_all_chacha20_implementations
def test_chacha20_encrypt(self):
key = bytes.fromhex('37326d9d69a83b815ddfd947d21b0dd39111e5b6a5a44042c44d570ea03e3179')
nonce = bytes.fromhex('0102030405060708')
data = bytes.fromhex('38a0e0a7c865fe9ca31f0730cfcab610f18e6da88dc3790f1d243f711a257c78')
self.assertEqual(bytes.fromhex('f62fbd74d197323c7c3d5658476a884d38ee6f4b5500add1e8dc80dcd9c15dff'),
crypto.chacha20_encrypt(key=key, nonce=nonce, data=data))
def test_sha256d(self): def test_sha256d(self):
self.assertEqual(b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4', self.assertEqual(b'\x95MZI\xfdp\xd9\xb8\xbc\xdb5\xd2R&x)\x95\x7f~\xf7\xfalt\xf8\x84\x19\xbd\xc5\xe8"\t\xf4',
sha256d(u"test")) sha256d(u"test"))

4
electrum/tests/test_lnpeer.py

@ -28,6 +28,7 @@ from electrum.logging import console_stderr_handler
from electrum.lnworker import PaymentInfo, RECEIVED, PR_UNPAID from electrum.lnworker import PaymentInfo, RECEIVED, PR_UNPAID
from .test_lnchannel import create_test_channels from .test_lnchannel import create_test_channels
from .test_bitcoin import needs_test_with_all_chacha20_implementations
from . import ElectrumTestCase from . import ElectrumTestCase
def keypair(): def keypair():
@ -246,6 +247,7 @@ class TestPeer(ElectrumTestCase):
with self.assertRaises(concurrent.futures.CancelledError): with self.assertRaises(concurrent.futures.CancelledError):
run(f()) run(f())
@needs_test_with_all_chacha20_implementations
def test_reestablish_with_old_state(self): def test_reestablish_with_old_state(self):
alice_channel, bob_channel = create_test_channels() alice_channel, bob_channel = create_test_channels()
alice_channel_0, bob_channel_0 = create_test_channels() # these are identical alice_channel_0, bob_channel_0 = create_test_channels() # these are identical
@ -279,6 +281,7 @@ class TestPeer(ElectrumTestCase):
with self.assertRaises(concurrent.futures.CancelledError): with self.assertRaises(concurrent.futures.CancelledError):
run(f()) run(f())
@needs_test_with_all_chacha20_implementations
def test_payment(self): def test_payment(self):
alice_channel, bob_channel = create_test_channels() alice_channel, bob_channel = create_test_channels()
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)
@ -293,6 +296,7 @@ class TestPeer(ElectrumTestCase):
with self.assertRaises(concurrent.futures.CancelledError): with self.assertRaises(concurrent.futures.CancelledError):
run(f()) run(f())
@needs_test_with_all_chacha20_implementations
def test_close(self): def test_close(self):
alice_channel, bob_channel = create_test_channels() alice_channel, bob_channel = create_test_channels()
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)

5
electrum/tests/test_lnrouter.py

@ -12,6 +12,8 @@ from electrum.constants import BitcoinTestnet
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from . import TestCaseForTestnet from . import TestCaseForTestnet
from .test_bitcoin import needs_test_with_all_chacha20_implementations
class Test_LNRouter(TestCaseForTestnet): class Test_LNRouter(TestCaseForTestnet):
@ -108,6 +110,7 @@ class Test_LNRouter(TestCaseForTestnet):
self._loop_thread.join(timeout=1) self._loop_thread.join(timeout=1)
cdb.sql_thread.join(timeout=1) cdb.sql_thread.join(timeout=1)
@needs_test_with_all_chacha20_implementations
def test_new_onion_packet(self): def test_new_onion_packet(self):
# test vector from bolt-04 # test vector from bolt-04
payment_path_pubkeys = [ payment_path_pubkeys = [
@ -140,6 +143,7 @@ class Test_LNRouter(TestCaseForTestnet):
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf'), self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf'),
packet.to_bytes()) packet.to_bytes())
@needs_test_with_all_chacha20_implementations
def test_process_onion_packet(self): def test_process_onion_packet(self):
# this test is not from bolt-04, but is based on the one there; # this test is not from bolt-04, but is based on the one there;
# except here we have the privkeys for these pubkeys # except here we have the privkeys for these pubkeys
@ -184,6 +188,7 @@ class Test_LNRouter(TestCaseForTestnet):
self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes()) self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes())
packet = processed_packet.next_packet packet = processed_packet.next_packet
@needs_test_with_all_chacha20_implementations
def test_decode_onion_error(self): def test_decode_onion_error(self):
# test vector from bolt-04 # test vector from bolt-04
payment_path_pubkeys = [ payment_path_pubkeys = [

3
electrum/tests/test_lntransport.py

@ -5,10 +5,12 @@ from electrum.lnutil import LNPeerAddr
from electrum.lntransport import LNResponderTransport, LNTransport from electrum.lntransport import LNResponderTransport, LNTransport
from . import ElectrumTestCase from . import ElectrumTestCase
from .test_bitcoin import needs_test_with_all_chacha20_implementations
class TestLNTransport(ElectrumTestCase): class TestLNTransport(ElectrumTestCase):
@needs_test_with_all_chacha20_implementations
def test_responder(self): def test_responder(self):
# local static # local static
ls_priv=bytes.fromhex('2121212121212121212121212121212121212121212121212121212121212121') ls_priv=bytes.fromhex('2121212121212121212121212121212121212121212121212121212121212121')
@ -37,6 +39,7 @@ class TestLNTransport(ElectrumTestCase):
transport = LNResponderTransport(ls_priv, Reader(), Writer()) transport = LNResponderTransport(ls_priv, Reader(), Writer())
asyncio.get_event_loop().run_until_complete(transport.handshake(epriv=e_priv)) asyncio.get_event_loop().run_until_complete(transport.handshake(epriv=e_priv))
@needs_test_with_all_chacha20_implementations
def test_loop(self): def test_loop(self):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
responder_shaked = asyncio.Event() responder_shaked = asyncio.Event()

7
setup.py

@ -53,8 +53,13 @@ if platform.system() in ['Linux', 'FreeBSD', 'DragonFly']:
extras_require = { extras_require = {
'hardware': requirements_hw, 'hardware': requirements_hw,
'gui': ['pyqt5'], 'gui': ['pyqt5'],
'crypto': ['pycryptodomex>=3.7'],
'tests': ['pycryptodomex>=3.7', 'cryptography>=2.1'],
} }
extras_require['full'] = [pkg for sublist in list(extras_require.values()) for pkg in sublist] # 'full' extra that tries to grab everything an enduser would need (except for libsecp256k1...)
extras_require['full'] = [pkg for sublist in ['hardware', 'gui', 'crypto'] for pkg in sublist]
# legacy. keep 'fast' extra working
extras_require['fast'] = extras_require['crypto']
setup( setup(

2
tox.ini

@ -9,4 +9,4 @@ commands=
coverage run --source=electrum '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' -m py.test -v coverage run --source=electrum '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' -m py.test -v
coverage report coverage report
extras= extras=
fast tests

Loading…
Cancel
Save