diff --git a/devp2p/crypto.py b/devp2p/crypto.py index 9e5b268..3582c0a 100644 --- a/devp2p/crypto.py +++ b/devp2p/crypto.py @@ -3,26 +3,11 @@ import warnings import os import sys -if sys.platform not in ('darwin',): - import pyelliptic -else: - # FIX PATH ON OS X () - # https://github.com/yann2192/pyelliptic/issues/11 - _openssl_lib_paths = ['/usr/local/Cellar/openssl/'] - for p in _openssl_lib_paths: - if os.path.exists(p): - p = os.path.join(p, os.listdir(p)[-1], 'lib') - os.environ['DYLD_LIBRARY_PATH'] = p - import pyelliptic - if CIPHERNAMES.issubset(set(pyelliptic.Cipher.get_all_cipher())): - break -if 'pyelliptic' not in dir() or not CIPHERNAMES.issubset(set(pyelliptic.Cipher.get_all_cipher())): - print('required ciphers %r not available in openssl library' % CIPHERNAMES) - if sys.platform == 'darwin': - print('use homebrew or macports to install newer openssl') - print('> brew install openssl / > sudo port install openssl') - sys.exit(1) - +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.constant_time import bytes_eq +from cryptography.hazmat.primitives import hashes, hmac +from cryptography.hazmat.backends import default_backend import bitcoin from Crypto.Hash import keccak from rlp.utils import str_to_bytes, safe_ord, ascii_chr @@ -30,22 +15,27 @@ from hashlib import sha256 import struct from coincurve import PrivateKey, PublicKey +from coincurve.utils import int_to_bytes_padded, bytes_to_int -hmac_sha256 = pyelliptic.hmac_sha256 +def hmac_sha256(key, msg): + mac = hmac.HMAC(key, hashes.SHA256(), default_backend()) + mac.update(msg) + return mac.finalize() class ECIESDecryptionError(RuntimeError): pass -class ECCx(pyelliptic.ECC): +class ECCx(object): """ Modified to work with raw_pubkey format used in RLPx and binding default curve and cipher """ - ecies_ciphername = 'aes-128-ctr' - curve = 'secp256k1' + ecies_cipher = algorithms.AES + ecies_mode = modes.CTR + curve = ec.SECP256K1 ecies_encrypt_overhead_length = 113 def __init__(self, raw_pubkey=None, raw_privkey=None): @@ -55,33 +45,31 @@ def __init__(self, raw_pubkey=None, raw_privkey=None): if raw_pubkey: assert len(raw_pubkey) == 64 _, pubkey_x, pubkey_y, _ = self._decode_pubkey(raw_pubkey) + pub_x = bytes_to_int(pubkey_x) + pub_y = bytes_to_int(pubkey_y) + pub_nums = ec.EllipticCurvePublicNumbers(pub_x, pub_y, + self.curve()) + else: + pub_nums = None + + if raw_pubkey and not raw_privkey: + self.pubkey = pub_nums.public_key(default_backend()) else: - pubkey_x, pubkey_y = None, None - while True: - pyelliptic.ECC.__init__(self, pubkey_x=pubkey_x, pubkey_y=pubkey_y, - raw_privkey=raw_privkey, curve=self.curve) - # XXX: when raw_privkey is generated by pyelliptic it sometimes - # has 31 bytes so we try again! - if self.raw_privkey and len(self.raw_privkey) != 32: - continue - try: - if self.raw_privkey: - bitcoin.get_privkey_format(self.raw_privkey) # failed for some keys - valid_priv_key = True - except AssertionError: - valid_priv_key = False - if len(self.raw_pubkey) == 64 and valid_priv_key: - break - elif raw_privkey or raw_pubkey: - raise Exception('invalid priv or pubkey') + if raw_privkey: + priv_num = bytes_to_int(raw_privkey) + priv_nums = ec.EllipticCurvePrivateNumbers(priv_num, pub_nums) + self.privkey = priv_nums.private_key(default_backend()) + else: + self.privkey = ec.generate_private_key(self.curve(), + default_backend()) + self.pubkey = self.privkey.public_key() assert len(self.raw_pubkey) == 64 @property def raw_pubkey(self): - if self.pubkey_x and self.pubkey_y: - return str_to_bytes(self.pubkey_x + self.pubkey_y) - return self.pubkey_x + self.pubkey_y + pub_nums = self.pubkey.public_numbers() + return int_to_bytes_padded(pub_nums.x) + int_to_bytes_padded(pub_nums.y) @classmethod def _decode_pubkey(cls, raw_pubkey): @@ -92,21 +80,28 @@ def _decode_pubkey(cls, raw_pubkey): def get_ecdh_key(self, raw_pubkey): "Compute public key with the local private key and returns a 256bits shared key" - _, pubkey_x, pubkey_y, _ = self._decode_pubkey(raw_pubkey) - key = self.raw_get_ecdh_key(pubkey_x, pubkey_y) + peer_pub = ECCx(raw_pubkey=raw_pubkey) + key = self.privkey.exchange(ec.ECDH(), peer_pub.pubkey) assert len(key) == 32 return key @property def raw_privkey(self): if self.privkey: - return str_to_bytes(self.privkey) + return int_to_bytes_padded( + self.privkey.private_numbers().private_value) return self.privkey def is_valid_key(self, raw_pubkey, raw_privkey=None): try: assert len(raw_pubkey) == 64 - failed = bool(self.raw_check_key(raw_privkey, raw_pubkey[:32], raw_pubkey[32:])) + failed = False + # this will check the pubkey when cryptography calls EC_KEY_set_public_key_affine_coordinates + ECCx(raw_pubkey=raw_pubkey) + if raw_privkey: + # make sure the privkey corresponds to pubkey + assert bytes_eq(ECCx(raw_privkey=raw_privkey).raw_pubkey, raw_pubkey) + failed = False except (AssertionError, Exception): failed = True return not failed @@ -131,11 +126,13 @@ def ecies_encrypt(cls, data, raw_pubkey, shared_mac_data=''): } """ + data = str_to_bytes(data) + # 1) generate r = random value ephem = ECCx() # 2) generate shared-secret = kdf( ecdhAgree(r, P) ) - key_material = ephem.raw_get_ecdh_key(pubkey_x=raw_pubkey[:32], pubkey_y=raw_pubkey[32:]) + key_material = ephem.get_ecdh_key(raw_pubkey) assert len(key_material) == 32 key = eciesKDF(key_material, 32) assert len(key) == 32 @@ -147,10 +144,11 @@ def ecies_encrypt(cls, data, raw_pubkey, shared_mac_data=''): ephem_pubkey = ephem.raw_pubkey # encrypt - iv = pyelliptic.Cipher.gen_IV(cls.ecies_ciphername) + algo = cls.ecies_cipher(key_enc) + iv = os.urandom(algo.block_size // 8) assert len(iv) == 16 - ctx = pyelliptic.Cipher(key_enc, iv, 1, cls.ecies_ciphername) - ciphertext = ctx.ciphering(data) + ctx = Cipher(algo, cls.ecies_mode(iv), default_backend()).encryptor() + ciphertext = ctx.update(data) + ctx.finalize() assert len(ciphertext) == len(data) # 4) send 0x04 || R || AsymmetricEncrypt(shared-secret, plaintext) || tag @@ -178,6 +176,8 @@ def ecies_decrypt(self, data, shared_mac_data=b''): [where R = r*G, and recipientPublic = recipientPrivate*G] """ + data = str_to_bytes(data) + if data[:1] != b'\x04': raise ECIESDecryptionError("wrong ecies header") @@ -185,7 +185,7 @@ def ecies_decrypt(self, data, shared_mac_data=b''): _shared = data[1:1 + 64] # FIXME, check that _shared_pub is a valid one (on curve) - key_material = self.raw_get_ecdh_key(pubkey_x=_shared[:32], pubkey_y=_shared[32:]) + key_material = self.get_ecdh_key(_shared) assert len(key_material) == 32 key = eciesKDF(key_material, 32) assert len(key) == 32 @@ -198,17 +198,18 @@ def ecies_decrypt(self, data, shared_mac_data=b''): assert len(tag) == 32 # 2) verify tag - if not pyelliptic.equals(hmac_sha256(key_mac, data[1 + 64:- 32] + shared_mac_data), tag): + if not bytes_eq(hmac_sha256(key_mac, data[1 + 64:- 32] + shared_mac_data), tag): raise ECIESDecryptionError("Fail to verify data") # 3) decrypt - blocksize = pyelliptic.OpenSSL.get_cipher(self.ecies_ciphername).get_blocksize() + algo = self.ecies_cipher(key_enc) + blocksize = algo.block_size // 8 iv = data[1 + 64:1 + 64 + blocksize] assert len(iv) == 16 ciphertext = data[1 + 64 + blocksize:- 32] assert 1 + len(_shared) + len(iv) + len(ciphertext) + len(tag) == len(data) - ctx = pyelliptic.Cipher(key_enc, iv, 0, self.ecies_ciphername) - return ctx.ciphering(ciphertext) + ctx = Cipher(algo, self.ecies_mode(iv), default_backend()).decryptor() + return ctx.update(ciphertext) + ctx.finalize() encrypt = ecies_encrypt decrypt = ecies_decrypt diff --git a/devp2p/rlpxcipher.py b/devp2p/rlpxcipher.py index adafff3..797ea84 100644 --- a/devp2p/rlpxcipher.py +++ b/devp2p/rlpxcipher.py @@ -10,7 +10,8 @@ from devp2p.crypto import ECCx from devp2p.crypto import ecdsa_recover from devp2p.crypto import ecdsa_verify -import pyelliptic +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend from devp2p.utils import ienc # integer encode import Crypto.Cipher.AES as AES @@ -405,11 +406,14 @@ def setup_cipher(self): else: self.egress_mac, self.ingress_mac = mac2, mac1 - ciphername = 'aes-256-ctr' - iv = "\x00" * 16 + # Yes, the encryption is insecure, see: + # https://github.com/ethereum/devp2p/issues/32 + iv = b'\x00' * 16 assert len(iv) == 16 - self.aes_enc = pyelliptic.Cipher(self.aes_secret, iv, 1, ciphername=ciphername) - self.aes_dec = pyelliptic.Cipher(self.aes_secret, iv, 0, ciphername=ciphername) + cipher = Cipher(algorithms.AES(self.aes_secret), modes.CTR(iv), default_backend()) + self.aes_enc = cipher.encryptor() + self.aes_dec = cipher.decryptor() + self.mac_enc = AES.new(self.mac_secret, AES.MODE_ECB).encrypt self.is_ready = True diff --git a/devp2p/tests/test_ecies.py b/devp2p/tests/test_ecies.py index 1bd988a..59b8af7 100644 --- a/devp2p/tests/test_ecies.py +++ b/devp2p/tests/test_ecies.py @@ -63,7 +63,7 @@ def test_agree(): "0xf0d2b97981bd0d415a843b5dfe8ab77a30300daab3658c578f2340308a2da1a07f0821367332598b6aa4e180a41e92f4ebbae3518da847f0b1c0bbfe20bcf4e1") agreeExpected = fromHex("0xee1418607c2fcfb57fda40380e885a707f49000a5dda056d828b7d9bd1f29a08") e = crypto.ECCx(raw_privkey=secret) - agreeTest = e.raw_get_ecdh_key(pubkey_x=public[:32], pubkey_y=public[32:]) + agreeTest = e.get_ecdh_key(public) assert(agreeExpected == agreeTest) diff --git a/devp2p/tests/test_go_signature.py b/devp2p/tests/test_go_signature.py index f05ba3c..6588e8b 100644 --- a/devp2p/tests/test_go_signature.py +++ b/devp2p/tests/test_go_signature.py @@ -1,17 +1,5 @@ from devp2p.crypto import ecdsa_sign, mk_privkey, privtopub, ecdsa_recover, ECCx from rlp.utils import decode_hex -import pyelliptic - - -def test_pyelliptic_sig(): - priv_seed = 'test' - priv_key = mk_privkey(priv_seed) - my_pubkey = privtopub(priv_key) - e = ECCx(raw_privkey=priv_key) - msg = 'a' - s = pyelliptic.ECC.sign(e, msg) - s2 = pyelliptic.ECC.sign(e, msg) - assert s != s2 # non deterministic def test_go_sig(): diff --git a/requirements.txt b/requirements.txt index ad2c93d..9b9fd20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -pyelliptic==1.5.7 wheel gevent>=1.1.0 bitcoin @@ -10,3 +9,4 @@ pycryptodome>=3.3.1 rlp>=0.5.1,<0.6.0 miniupnpc repoze.lru +cryptography>=1.8.0