From db2df15d684a2b8abd189e6130ac28f6cd724263 Mon Sep 17 00:00:00 2001 From: Sefik Ilkin Serengil Date: Fri, 26 Jan 2024 17:33:10 +0000 Subject: [PATCH] january 26 improvements --- .gitignore | 3 +- README.md | 34 +++++++++++++++++------ lightphe/__init__.py | 13 +++++++-- lightphe/cryptosystems/OkamotoUchiyama.py | 17 +++++++----- setup.py | 3 +- tests/test_cloud.py | 11 ++++---- tests/test_exporting_keys.py | 26 +++++++++++++++++ 7 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 tests/test_exporting_keys.py diff --git a/.gitignore b/.gitignore index 74b3e5e..122b811 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ lightphe.egg-info **/.DS_Store build/ dist/ -*.pyc \ No newline at end of file +*.pyc +tests/*.lphe \ No newline at end of file diff --git a/README.md b/README.md index 3d1fbd8..301105e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/lightphe?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pypi%20downloads)](https://pepy.tech/project/lightphe) -[![Stars](https://img.shields.io/github/stars/serengil/LightPHE?color=yellow&style=flat)](https://github.com/serengil/LightPHE/stargazers) +[![Stars](https://img.shields.io/github/stars/serengil/LightPHE?color=yellow&style=flat&label=%E2%AD%90%20stars)](https://github.com/serengil/LightPHE/stargazers) [![Tests](https://github.com/serengil/LightPHE/actions/workflows/tests.yml/badge.svg)](https://github.com/serengil/LightPHE/actions/workflows/tests.yml) [![License](http://img.shields.io/:license-MIT-green.svg?style=flat)](https://github.com/serengil/LightPHE/blob/master/LICENSE) [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dserengil%26type%3Dpatrons&style=flat)](https://www.patreon.com/serengil?repo=lightphe) @@ -25,11 +25,10 @@ Even though fully homomorphic encryption (FHE) has become available in recent ti - 🏎️ Notably faster - 💻 Demands fewer computational resources -- 📏 Generating smaller ciphertexts +- 📏 Generating much smaller ciphertexts +- 🔑 Distributing much smaller keys - 🧠 Well-suited for memory-constrained environments - ⚖️ Strikes a favorable balance for practical use cases -- 🔑 Supporting encryption and decryption of vectors -- 🗝️ Performing homomorphic addition, homomorphic element-wise multiplication and scalar multiplication on encrypted vectors # Installation [![PyPI](https://img.shields.io/pypi/v/lightphe.svg)](https://pypi.org/project/lightphe/) @@ -161,14 +160,14 @@ However, if you tried to multiply ciphertexts with RSA, or xor ciphertexts with # Working with vectors -You can encrypt the output vectors of machine learning models with LightPHE. These encrypted tensors come with homomorphic operation support. +You can encrypt the output vectors of machine learning models with LightPHE. These encrypted tensors come with homomorphic operation support including homomorphic addition, element-wise multiplication and scalar multiplication. ```python # build an additively homomorphic cryptosystem cs = LightPHE(algorithm_name="Paillier") # define plain tensors -t1 = [1.005, 2.05, -3.5, 4] +t1 = [1.005, 2.05, 3.5, 4] t2 = [5, 6.2, 7.002, 8.02] # encrypt tensors @@ -178,13 +177,32 @@ c2 = cs.encrypt(t2) # perform homomorphic addition c3 = c1 + c2 +# perform homomorphic element-wise multiplication +c4 = c1 * c2 + +# perform homomorphic scalar multiplication +k = 5 +c5 = k * c1 + # decrypt the addition tensor t3 = cs.decrypt(c3) -for i, tensor in enumerate(t3): - assert abs((t1[i] + t2[i]) - restored_tensor) < 0.5 +# decrypt the element-wise multiplied tensor +t4 = cs.decrypt(c4) + +# decrypt the scalar multiplied tensor +t5 = cs.decrypt(c5) + +# data validations +threshold = 0.5 +for i in range(0, len(t1)): + assert abs((t1[i] + t2[i]) - t3[i]) < threshold + assert abs((t1[i] * t2[i]) - t4[i]) < threshold + assert abs((t1[i] * k) - t5[i]) < threshold ``` +Unfortunately, vector multiplication (dot product) requires both homomorphic addition and homomorphic multiplication and this cannot be done with partially homomorphic encryption algorithms. + # Contributing All PRs are more than welcome! If you are planning to contribute a large patch, please create an issue first to get any upfront questions or design decisions out of the way first. diff --git a/lightphe/__init__.py b/lightphe/__init__.py index 0a46fad..c54d51e 100644 --- a/lightphe/__init__.py +++ b/lightphe/__init__.py @@ -116,8 +116,8 @@ def encrypt(self, plaintext: Union[int, float, list]) -> Union[Ciphertext, Encry Returns ciphertext (from lightphe.models.Ciphertext import Ciphertext): encrypted message """ - if self.cs.keys.get("private_key") is None: - raise ValueError("You must have private key to perform encryption") + if self.cs.keys.get("public_key") is None: + raise ValueError("You must have public key to perform encryption") if isinstance(plaintext, list): # then encrypt tensors @@ -141,6 +141,9 @@ def decrypt( if self.cs.keys.get("private_key") is None: raise ValueError("You must have private key to perform decryption") + if self.cs.keys.get("public_key") is None: + raise ValueError("You must have public key to perform decryption") + if isinstance(ciphertext, EncryptedTensor): # then this is encrypted tensor return self.__decrypt_tensors(encrypted_tensor=ciphertext) @@ -243,7 +246,9 @@ def export_keys(self, target_file: str, public: bool = False) -> None: to publicly. """ keys = self.cs.keys + private_key = None if public is True and keys.get("private_key") is not None: + private_key = keys["private_key"] del keys["private_key"] if public is False: @@ -255,6 +260,10 @@ def export_keys(self, target_file: str, public: bool = False) -> None: with open(target_file, "w", encoding="UTF-8") as file: file.write(json.dumps(keys)) + # restore private key if you dropped + if private_key is not None: + self.cs.keys["private_key"] = private_key + def restore_keys(self, target_file: str) -> dict: """ Restore keys from a file diff --git a/lightphe/cryptosystems/OkamotoUchiyama.py b/lightphe/cryptosystems/OkamotoUchiyama.py index 671d19d..d6af543 100644 --- a/lightphe/cryptosystems/OkamotoUchiyama.py +++ b/lightphe/cryptosystems/OkamotoUchiyama.py @@ -81,18 +81,21 @@ def encrypt(self, plaintext: int, random_key: Optional[int] = None) -> int: Returns: ciphertext (int): encrypted message """ - p = self.keys["private_key"]["p"] + g = self.keys["public_key"]["g"] n = self.keys["public_key"]["n"] h = self.keys["public_key"]["h"] r = random_key or self.generate_random_key() - if plaintext > p: - plaintext = plaintext % p - logger.debug( - f"plaintext must be in scale [0, {p=}] but this is exceeded." - "New plaintext is {plaintext}" - ) + # having private key is not a must to encrypt but still if you have + if self.keys.get("private_key") is not None: + p = self.keys["private_key"]["p"] + if plaintext > p: + plaintext = plaintext % p + logger.debug( + f"plaintext must be in scale [0, {p=}] but this is exceeded." + "New plaintext is {plaintext}" + ) return (pow(g, plaintext, n) * pow(h, r, n)) % n def decrypt(self, ciphertext: int): diff --git a/setup.py b/setup.py index 33cfe90..261f0fd 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,11 @@ setuptools.setup( name="lightphe", - version="0.0.5", + version="0.0.6", author="Sefik Ilkin Serengil", author_email="serengil@gmail.com", description="A Lightweight Partially Homomorphic Encryption Library for Python", + data_files=[("", ["README.md", "requirements.txt"])], long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/serengil/LightPHE", diff --git a/tests/test_cloud.py b/tests/test_cloud.py index 821394b..6fc546e 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -10,7 +10,10 @@ def test_encryption(): - cs = LightPHE(algorithm_name="RSA", keys=PRIVATE) + # i actually have both private and public key + # but one can encrypt messages with public key only + cs = LightPHE(algorithm_name="RSA", keys=PUBLIC) + secret_cs = LightPHE(algorithm_name="RSA", keys=PRIVATE) # plaintexts m1 = 10000 @@ -19,15 +22,13 @@ def test_encryption(): m2 = 1.05 c2 = cs.encrypt(m2) - assert cs.decrypt(c1) == m1 - + assert secret_cs.decrypt(c1) == m1 logger.info("✅ Cloud encryption tests done") c3_val = homomorphic_operations(c1=c1.value, c2=c2.value) c3 = cs.create_ciphertext_obj(c3_val) - assert cs.decrypt(c3) == m1 * m2 - + assert secret_cs.decrypt(c3) == m1 * m2 logger.info("✅ Cloud decryption tests done") diff --git a/tests/test_exporting_keys.py b/tests/test_exporting_keys.py new file mode 100644 index 0000000..0558fea --- /dev/null +++ b/tests/test_exporting_keys.py @@ -0,0 +1,26 @@ +import os +from lightphe import LightPHE +from lightphe.commons.logger import Logger + +logger = Logger(module="tests/test_goldwasser.py") + + +# pylint: disable=eval-used +def test_private_available_after_export(): + target_file = "my_public_key.lphe" + cs = LightPHE(algorithm_name="RSA") + # we are dropping private key while exporting public key + cs.export_keys(public=True, target_file=target_file) + assert cs.cs.keys.get("private_key") is not None + logger.info("✅ private key is not available in public key file as expected") + + with open(target_file, "r", encoding="UTF-8") as file: + key_str = file.read() + keys = eval(key_str) + assert keys.get("private_key") is None + logger.info( + "✅ private key is available in cryptosystem's keys after" + "its public key exported as expected" + ) + + os.remove(target_file)