Skip to content

Commit

Permalink
Replace pyelliptic with pyca/cryptography
Browse files Browse the repository at this point in the history
  • Loading branch information
Wolf480pl committed Sep 13, 2017
1 parent e1ef07a commit 1857e20
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 77 deletions.
119 changes: 61 additions & 58 deletions devp2p/crypto.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,42 @@
#!/usr/bin/python
from cryptography.hazmat.primitives.ciphers import algorithms
CIPHERNAMES = set(('aes-128-ctr',))
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 +46,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 = int.from_bytes(pubkey_x, 'big')
pub_y = int.from_bytes(pubkey_y, 'big')
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 = int.from_bytes(raw_privkey, 'big')
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 pub_nums.x.to_bytes(32, 'big') + pub_nums.y.to_bytes(32, 'big')

@classmethod
def _decode_pubkey(cls, raw_pubkey):
Expand All @@ -92,21 +81,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 = self.__class__(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 +127,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 +145,12 @@ 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)
#iv = pyelliptic.Cipher.gen_IV(cls.ecies_ciphername)
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 +178,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 +200,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
11 changes: 6 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,11 @@ def setup_cipher(self):
else:
self.egress_mac, self.ingress_mac = mac2, mac1

ciphername = 'aes-256-ctr'
iv = "\x00" * 16
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
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

0 comments on commit 1857e20

Please sign in to comment.