Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace pyelliptic with pyca/cryptography #81

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 59 additions & 58 deletions devp2p/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,39 @@
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
sha3_256 = lambda x: keccak.new(digest_bits=256, data=str_to_bytes(x))
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):
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -178,14 +176,16 @@ 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")

# 1) generate shared-secret = kdf( ecdhAgree(myPrivKey, msg[1:65]) )
_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
Expand All @@ -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
Expand Down
14 changes: 9 additions & 5 deletions devp2p/rlpxcipher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Copy link

@gsalgado gsalgado Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woudln't it be possible to use Cipher(algorithms.AES(self.mac_secret), modes.ECB(), default_backend()).encryptor().update here, to avoid depending on two different crypto libs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to keep my changes minimal.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, changing this line won't let us get rid of pycryptodome dependency - we also use it for keccak.


self.is_ready = True
2 changes: 1 addition & 1 deletion devp2p/tests/test_ecies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
12 changes: 0 additions & 12 deletions devp2p/tests/test_go_signature.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pyelliptic==1.5.7
wheel
gevent>=1.1.0
bitcoin
Expand All @@ -10,3 +9,4 @@ pycryptodome>=3.3.1
rlp>=0.5.1,<0.6.0
miniupnpc
repoze.lru
cryptography>=1.8.0