Repository: ecies/py

Issue:
Using proper KDF
Currently KDF is just SHA256, but there are a bunch of standards which defines what KDF **should be**. There is an old one ANSI-X9.63: http://www.secg.org/sec1-v2.pdf (clause 3.6.1).

But there is also newer ISO 18033-2 document that defines new KDF: https://www.shoup.net/iso/std6.pdf (clause 6.2). (Drafts contain discussion why not just Hash (https://www.shoup.net/papers/iso-2_1.pdf))

And pay an attention: Both documents define generic KDF for every algorithm described in the documents.

I guess we should replace current KDF with ISO 18033-2 variant. 

Comments:

Patch:
diff --git a/README.md b/README.md
index 76b13b0..592e497 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/ecies/__init__.py b/ecies/__init__.py
index 4323f0d..4f50bca 100644
--- a/ecies/__init__.py
+++ b/ecies/__init__.py
@@ -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"]
 
@@ -23,7 +23,6 @@ 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):
@@ -31,7 +30,7 @@ def encrypt(receiver_pk: Union[str, bytes], msg: bytes) -> bytes:
     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
 
@@ -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):
@@ -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)
diff --git a/ecies/__version__.py b/ecies/__version__.py
index 54e3abf..90f3874 100644
--- a/ecies/__version__.py
+++ b/ecies/__version__.py
@@ -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"
diff --git a/ecies/utils.py b/ecies/utils.py
index 414c5d7..2cae0d3 100644
--- a/ecies/utils.py
+++ b/ecies/utils.py
@@ -2,11 +2,14 @@ import hashlib
 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",
@@ -14,7 +17,6 @@ __all__ = [
     "generate_eth_key",
     "hex2prv",
     "hex2pub",
-    "derive",
     "aes_encrypt",
     "aes_decrypt",
 ]
@@ -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:


Test patch:
diff --git a/ecies/tests/test_crypt.py b/ecies/tests/test_crypt.py
index 7143031..74eee6b 100644
--- a/ecies/tests/test_crypt.py
+++ b/ecies/tests/test_crypt.py
@@ -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):
@@ -13,6 +17,27 @@ class TestCrypt(unittest.TestCase):
     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()