Skip to content

Add HKDF #87

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

Merged
merged 8 commits into from
Aug 16, 2019
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ b'helloworld'

## Release Notes

### 0.3.0

- API change: use `HKDF` to derive shared keys instead of `sha256`

### 0.2.0

- API change: `ecies.encrypt` and `ecies.decrypt` now can take both hex str and raw bytes
Expand Down
10 changes: 4 additions & 6 deletions ecies/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Union

from coincurve import PrivateKey, PublicKey
from ecies.utils import generate_key, hex2prv, hex2pub, derive, aes_encrypt, aes_decrypt
from ecies.utils import generate_key, hex2prv, hex2pub, encapsulate, decapsulate, aes_encrypt, aes_decrypt

__all__ = ["encrypt", "decrypt"]

Expand All @@ -23,15 +23,14 @@ def encrypt(receiver_pk: Union[str, bytes], msg: bytes) -> bytes:
Encrypted data
"""
ephemeral_key = generate_key()

if isinstance(receiver_pk, str):
receiver_pubkey = hex2pub(receiver_pk)
elif isinstance(receiver_pk, bytes):
receiver_pubkey = PublicKey(receiver_pk)
else:
raise TypeError("Invalid public key type")

aes_key = derive(ephemeral_key, receiver_pubkey)
aes_key = encapsulate(ephemeral_key, receiver_pubkey)
cipher_text = aes_encrypt(aes_key, msg)
return ephemeral_key.public_key.format(False) + cipher_text

Expand All @@ -52,7 +51,6 @@ def decrypt(receiver_sk: Union[str, bytes], msg: bytes) -> bytes:
bytes
Plain text
"""

if isinstance(receiver_sk, str):
private_key = hex2prv(receiver_sk)
elif isinstance(receiver_sk, bytes):
Expand All @@ -62,7 +60,7 @@ def decrypt(receiver_sk: Union[str, bytes], msg: bytes) -> bytes:

pubkey = msg[0:65] # uncompressed pubkey's length is 65 bytes
encrypted = msg[65:]
sender_public_key = PublicKey(pubkey)
ephemeral_public_key = PublicKey(pubkey)

aes_key = derive(private_key, sender_public_key)
aes_key = decapsulate(ephemeral_public_key, private_key)
return aes_decrypt(aes_key, encrypted)
2 changes: 1 addition & 1 deletion ecies/__version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__title__ = "eciespy"
__version__ = "0.2.0"
__version__ = "0.3.0"
__description__ = "Elliptic Curve Integrated Encryption Scheme for secp256k1 in Python"
__url__ = "https://github.com/ecies/py"
__author__ = "Weiliang Li"
Expand Down
27 changes: 26 additions & 1 deletion ecies/tests/test_crypt.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import unittest

from coincurve import PrivateKey
from Cryptodome.Protocol.KDF import HKDF
from Cryptodome.Hash import SHA256

from ecies import encrypt, decrypt
from ecies.utils import sha256, generate_eth_key, generate_key, aes_encrypt, aes_decrypt
from ecies.utils import sha256, encapsulate, decapsulate, generate_eth_key, generate_key, aes_encrypt, aes_decrypt


class TestCrypt(unittest.TestCase):
Expand All @@ -13,6 +17,27 @@ def setUp(self):
def test_hash(self):
self.assertEqual(sha256(b"0" * 16).hex()[:8], "fcdb4b42")

def test_hdkf(self):
derived = HKDF(b'secret', 32, b'', SHA256).hex()
self.assertEqual(
derived,
"2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf"
)

k1 = PrivateKey(secret=bytes([2]))
self.assertEqual(k1.to_int(), 2)

k2 = PrivateKey(secret=bytes([3]))
self.assertEqual(k2.to_int(), 3)

self.assertEqual(
encapsulate(k1, k2.public_key), decapsulate(k1.public_key, k2)
)
self.assertEqual(
encapsulate(k1, k2.public_key).hex(),
"6f982d63e8590c9d9b5b4c1959ff80315d772edd8f60287c9361d548d5200f82"
)

def test_elliptic(self):
data = self.test_string
k = generate_eth_key()
Expand Down
38 changes: 13 additions & 25 deletions ecies/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
import codecs

from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import HKDF
from Cryptodome.Hash import SHA256
from coincurve import PrivateKey, PublicKey
from coincurve.utils import get_valid_secret
from eth_keys import keys

AES_CIPHER_MODE = AES.MODE_GCM
AES_KEY_BYTES_LEN = 32

__all__ = [
"sha256",
"generate_key",
"generate_eth_key",
"hex2prv",
"hex2pub",
"derive",
"aes_encrypt",
"aes_decrypt",
]
Expand Down Expand Up @@ -136,32 +138,18 @@ def hex2prv(prv_hex: str) -> PrivateKey:
return PrivateKey(decode_hex(prv_hex))


def derive(private_key: PrivateKey, peer_public_key: PublicKey) -> bytes:
"""
Key exchange between private key and peer's public key,
`derive(k1, k2.public_key)` should be equal to `derive(k2, k1.public_key)`.
def encapsulate(private_key: PrivateKey, peer_public_key: PublicKey) -> bytes:
shared_point = peer_public_key.multiply(private_key.secret)
master = private_key.public_key.format(compressed=False) + shared_point.format(compressed=False)
derived = HKDF(master, AES_KEY_BYTES_LEN, b'', SHA256)
return derived

Parameters
----------
private_key: coincurve.PrivateKey
A secp256k1 private key
peer_public_key: coincurve.PublicKey
Peer's public key

Returns
-------
bytes
A secret key used for symmetric encryption

>>> from coincurve import PrivateKey
>>> ke1 = generate_eth_key()
>>> ke2 = generate_eth_key()
>>> k1 = hex2prv(ke1.to_hex())
>>> k2 = hex2prv(ke2.to_hex())
>>> derive(k1, k2.public_key) == derive(k2, k1.public_key)
True
"""
return private_key.ecdh(peer_public_key.format())
def decapsulate(public_key: PublicKey, peer_private_key: PrivateKey) -> bytes:
shared_point = public_key.multiply(peer_private_key.secret)
master = public_key.format(compressed=False) + shared_point.format(compressed=False)
derived = HKDF(master, AES_KEY_BYTES_LEN, b'', SHA256)
return derived


def aes_encrypt(key: bytes, plain_text: bytes) -> bytes:
Expand Down