lightphe 0.0.23__tar.gz → 0.0.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lightphe-0.0.23 → lightphe-0.0.25}/PKG-INFO +14 -19
- {lightphe-0.0.23 → lightphe-0.0.25}/README.md +13 -18
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/__init__.py +5 -2
- lightphe-0.0.25/lightphe/commons/key_validator.py +97 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/Benaloh.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/BonehGohNissim.py +21 -15
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/DamgardJurik.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/ElGamal.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/EllipticCurveElGamal.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/GoldwasserMicali.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/NaccacheStern.py +13 -6
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/OkamotoUchiyama.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/Paillier.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/RSA.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/SanderYoungYung.py +5 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/models/Homomorphic.py +7 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe.egg-info/PKG-INFO +14 -19
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe.egg-info/SOURCES.txt +2 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe.egg-info/requires.txt +1 -1
- {lightphe-0.0.23 → lightphe-0.0.25}/requirements.txt +1 -1
- {lightphe-0.0.23 → lightphe-0.0.25}/setup.py +1 -1
- lightphe-0.0.25/tests/test_key_validator.py +100 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_naccache.py +4 -5
- {lightphe-0.0.23 → lightphe-0.0.25}/LICENSE +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/commons/__init__.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/commons/logger.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/commons/phe_utils.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/cryptosystems/__init__.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/models/Algorithm.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/models/Ciphertext.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/models/Tensor.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe/models/__init__.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe.egg-info/dependency_links.txt +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/lightphe.egg-info/top_level.txt +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/setup.cfg +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/__init__.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_benaloh.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_bonehgohnissim.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_ciphertexts.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_cloud.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_damgard.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_elgamal.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_ellipticcurveelgamal.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_exporting_keys.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_goldwasser.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_keys.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_large_keys.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_okamoto.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_paillier.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_rsa.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_salary.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_sanderyoungyung.py +0 -0
- {lightphe-0.0.23 → lightphe-0.0.25}/tests/test_tensors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lightphe
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.25
|
|
4
4
|
Summary: A Lightweight Partially Homomorphic Encryption Library for Python
|
|
5
5
|
Home-page: https://github.com/serengil/LightPHE
|
|
6
6
|
Author: Sefik Ilkin Serengil
|
|
@@ -65,7 +65,7 @@ from lightphe import LightPHE
|
|
|
65
65
|
|
|
66
66
|
# Summary of Homomorphic Features of Different Cryptosystems in LightPHE
|
|
67
67
|
|
|
68
|
-
In summary, LightPHE is covering following algorithms and these are partially homomorphic with respect to the operations mentioned in the following table.
|
|
68
|
+
In summary, LightPHE is covering following algorithms and these are partially homomorphic and somewhat homomorphic with respect to the operations mentioned in the following table.
|
|
69
69
|
|
|
70
70
|
| Algorithm | Multiplicatively<br>Homomorphic | Additively<br>Homomorphic | Scalar Multiplication | Bitwise-XOR Homomorphic | Bitwise-AND Homomorphic | Regeneration<br>of Ciphertext |
|
|
71
71
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -156,7 +156,7 @@ assert cs.decrypt(c3) == m1 * m2
|
|
|
156
156
|
|
|
157
157
|
### Ciphertext Regeneration
|
|
158
158
|
|
|
159
|
-
The most of
|
|
159
|
+
The most of homomorphic algorithms allow you to regenerate ciphertext while you are not breaking its plaintext restoration. You may consider to do this re-generation many times to have stronger ciphertexts.
|
|
160
160
|
|
|
161
161
|
```python
|
|
162
162
|
c1_prime = cs.regenerate_ciphertext(c1)
|
|
@@ -248,22 +248,17 @@ Also, your company's logo will be shown on README on GitHub if you become sponso
|
|
|
248
248
|
Please cite LightPHE in your publications if it helps your research. Here is its BibTex entry:
|
|
249
249
|
|
|
250
250
|
```BibTeX
|
|
251
|
-
@article{
|
|
252
|
-
title={
|
|
253
|
-
author={
|
|
254
|
-
journal={
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
title={Encrypted Vector Similarity Computations Using Partially Homomorphic Encryption: Applications and Performance Analysis},
|
|
263
|
-
author={Serengil, Sefik and Ozpinar, Alper},
|
|
264
|
-
journal={arXiv preprint arXiv:2503.05850},
|
|
265
|
-
note={doi: 10.48550/arXiv.2503.05850. [Online]. Available: \url{https://arxiv.org/abs/2503.05850}},
|
|
266
|
-
year={2025}
|
|
251
|
+
@article{sym18050832,
|
|
252
|
+
title = {Sustainable Cryptography: Carbon Asymmetry in Partially Homomorphic Encryption in the Cloud},
|
|
253
|
+
author = {Ozpinar, Alper and Serengil, Sefik Ilkin},
|
|
254
|
+
journal = {Symmetry},
|
|
255
|
+
volume = {18},
|
|
256
|
+
number = {5},
|
|
257
|
+
pages = {832},
|
|
258
|
+
url = {https://www.mdpi.com/2073-8994/18/5/832},
|
|
259
|
+
doi = {10.3390/sym18050832},
|
|
260
|
+
note = {Special Issue: Symmetry in Cryptography and Cybersecurity},
|
|
261
|
+
year = {2026}
|
|
267
262
|
}
|
|
268
263
|
```
|
|
269
264
|
|
|
@@ -49,7 +49,7 @@ from lightphe import LightPHE
|
|
|
49
49
|
|
|
50
50
|
# Summary of Homomorphic Features of Different Cryptosystems in LightPHE
|
|
51
51
|
|
|
52
|
-
In summary, LightPHE is covering following algorithms and these are partially homomorphic with respect to the operations mentioned in the following table.
|
|
52
|
+
In summary, LightPHE is covering following algorithms and these are partially homomorphic and somewhat homomorphic with respect to the operations mentioned in the following table.
|
|
53
53
|
|
|
54
54
|
| Algorithm | Multiplicatively<br>Homomorphic | Additively<br>Homomorphic | Scalar Multiplication | Bitwise-XOR Homomorphic | Bitwise-AND Homomorphic | Regeneration<br>of Ciphertext |
|
|
55
55
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -140,7 +140,7 @@ assert cs.decrypt(c3) == m1 * m2
|
|
|
140
140
|
|
|
141
141
|
### Ciphertext Regeneration
|
|
142
142
|
|
|
143
|
-
The most of
|
|
143
|
+
The most of homomorphic algorithms allow you to regenerate ciphertext while you are not breaking its plaintext restoration. You may consider to do this re-generation many times to have stronger ciphertexts.
|
|
144
144
|
|
|
145
145
|
```python
|
|
146
146
|
c1_prime = cs.regenerate_ciphertext(c1)
|
|
@@ -232,22 +232,17 @@ Also, your company's logo will be shown on README on GitHub if you become sponso
|
|
|
232
232
|
Please cite LightPHE in your publications if it helps your research. Here is its BibTex entry:
|
|
233
233
|
|
|
234
234
|
```BibTeX
|
|
235
|
-
@article{
|
|
236
|
-
title={
|
|
237
|
-
author={
|
|
238
|
-
journal={
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
title={Encrypted Vector Similarity Computations Using Partially Homomorphic Encryption: Applications and Performance Analysis},
|
|
247
|
-
author={Serengil, Sefik and Ozpinar, Alper},
|
|
248
|
-
journal={arXiv preprint arXiv:2503.05850},
|
|
249
|
-
note={doi: 10.48550/arXiv.2503.05850. [Online]. Available: \url{https://arxiv.org/abs/2503.05850}},
|
|
250
|
-
year={2025}
|
|
235
|
+
@article{sym18050832,
|
|
236
|
+
title = {Sustainable Cryptography: Carbon Asymmetry in Partially Homomorphic Encryption in the Cloud},
|
|
237
|
+
author = {Ozpinar, Alper and Serengil, Sefik Ilkin},
|
|
238
|
+
journal = {Symmetry},
|
|
239
|
+
volume = {18},
|
|
240
|
+
number = {5},
|
|
241
|
+
pages = {832},
|
|
242
|
+
url = {https://www.mdpi.com/2073-8994/18/5/832},
|
|
243
|
+
doi = {10.3390/sym18050832},
|
|
244
|
+
note = {Special Issue: Symmetry in Cryptography and Cybersecurity},
|
|
245
|
+
year = {2026}
|
|
251
246
|
}
|
|
252
247
|
```
|
|
253
248
|
|
|
@@ -16,6 +16,7 @@ from lightphe.models.Ciphertext import Ciphertext
|
|
|
16
16
|
from lightphe.models.Algorithm import Algorithm
|
|
17
17
|
from lightphe.models.Tensor import Fraction, EncryptedTensor
|
|
18
18
|
from lightphe.commons import phe_utils
|
|
19
|
+
from lightphe.commons.key_validator import validate_keys
|
|
19
20
|
from lightphe.commons.logger import Logger
|
|
20
21
|
|
|
21
22
|
# cryptosystems
|
|
@@ -31,12 +32,11 @@ from lightphe.cryptosystems.EllipticCurveElGamal import EllipticCurveElGamal
|
|
|
31
32
|
from lightphe.cryptosystems.SanderYoungYung import SanderYoungYung
|
|
32
33
|
from lightphe.cryptosystems.BonehGohNissim import BonehGohNissim
|
|
33
34
|
|
|
34
|
-
|
|
35
35
|
# pylint: disable=eval-used, simplifiable-if-expression, too-few-public-methods
|
|
36
36
|
|
|
37
37
|
logger = Logger(module="lightphe/__init__.py")
|
|
38
38
|
|
|
39
|
-
VERSION = "0.0.
|
|
39
|
+
VERSION = "0.0.25"
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class LightPHE:
|
|
@@ -90,6 +90,9 @@ class LightPHE:
|
|
|
90
90
|
if key_file is not None:
|
|
91
91
|
keys = self.restore_keys(target_file=key_file)
|
|
92
92
|
|
|
93
|
+
if keys is not None:
|
|
94
|
+
validate_keys(algorithm_name=algorithm_name, keys=keys)
|
|
95
|
+
|
|
93
96
|
self.cs: Homomorphic = self.__build_cryptosystem(
|
|
94
97
|
algorithm_name=algorithm_name,
|
|
95
98
|
keys=keys,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Dict, Type
|
|
2
|
+
|
|
3
|
+
from lightphe.models.Algorithm import Algorithm
|
|
4
|
+
from lightphe.models.Homomorphic import Homomorphic
|
|
5
|
+
from lightphe.cryptosystems.RSA import RSA
|
|
6
|
+
from lightphe.cryptosystems.ElGamal import ElGamal
|
|
7
|
+
from lightphe.cryptosystems.EllipticCurveElGamal import EllipticCurveElGamal
|
|
8
|
+
from lightphe.cryptosystems.Paillier import Paillier
|
|
9
|
+
from lightphe.cryptosystems.DamgardJurik import DamgardJurik
|
|
10
|
+
from lightphe.cryptosystems.OkamotoUchiyama import OkamotoUchiyama
|
|
11
|
+
from lightphe.cryptosystems.Benaloh import Benaloh
|
|
12
|
+
from lightphe.cryptosystems.NaccacheStern import NaccacheStern
|
|
13
|
+
from lightphe.cryptosystems.GoldwasserMicali import GoldwasserMicali
|
|
14
|
+
from lightphe.cryptosystems.SanderYoungYung import SanderYoungYung
|
|
15
|
+
from lightphe.cryptosystems.BonehGohNissim import BonehGohNissim
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Map user-facing algorithm names to the class that owns the REQUIRED_KEYS spec.
|
|
19
|
+
# The spec itself lives as a class attribute on each cryptosystem.
|
|
20
|
+
# Exponential-ElGamal reuses ElGamal's key structure.
|
|
21
|
+
_ALGORITHM_TO_CLASS: Dict[str, Type[Homomorphic]] = {
|
|
22
|
+
Algorithm.RSA: RSA,
|
|
23
|
+
Algorithm.ElGamal: ElGamal,
|
|
24
|
+
Algorithm.ExponentialElGamal: ElGamal,
|
|
25
|
+
Algorithm.EllipticCurveElGamal: EllipticCurveElGamal,
|
|
26
|
+
Algorithm.Paillier: Paillier,
|
|
27
|
+
Algorithm.DamgardJurik: DamgardJurik,
|
|
28
|
+
Algorithm.OkamotoUchiyama: OkamotoUchiyama,
|
|
29
|
+
Algorithm.Benaloh: Benaloh,
|
|
30
|
+
Algorithm.NaccacheStern: NaccacheStern,
|
|
31
|
+
Algorithm.GoldwasserMicali: GoldwasserMicali,
|
|
32
|
+
Algorithm.SanderYoungYung: SanderYoungYung,
|
|
33
|
+
Algorithm.BonehGohNissim: BonehGohNissim,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_keys(algorithm_name: str, keys: dict) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Validate that user-supplied keys carry the required sub-fields for the
|
|
40
|
+
given cryptosystem. `public_key` is always required (every cryptosystem
|
|
41
|
+
needs it for encryption); `private_key` is optional (absent in
|
|
42
|
+
encryption-only setups). Whichever is present must contain all its
|
|
43
|
+
required sub-fields.
|
|
44
|
+
|
|
45
|
+
The required-field spec is read from the cryptosystem class's
|
|
46
|
+
`REQUIRED_KEYS` attribute (a dict with "public_key", "private_key", and
|
|
47
|
+
an optional "nested" entry for dict-valued public_key sub-fields).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
algorithm_name (str): user-facing algorithm name (e.g. "RSA")
|
|
51
|
+
keys (dict): user-supplied keys dict
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: when required fields are missing or structure is wrong
|
|
54
|
+
"""
|
|
55
|
+
cls = _ALGORITHM_TO_CLASS.get(algorithm_name)
|
|
56
|
+
if cls is None:
|
|
57
|
+
return
|
|
58
|
+
spec = cls.REQUIRED_KEYS
|
|
59
|
+
|
|
60
|
+
if not isinstance(keys, dict):
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"{algorithm_name} keys must be a dict, got {type(keys).__name__}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if "public_key" not in keys:
|
|
66
|
+
raise ValueError(f"{algorithm_name} keys must contain 'public_key'")
|
|
67
|
+
|
|
68
|
+
for kind in ("public_key", "private_key"):
|
|
69
|
+
if kind not in keys:
|
|
70
|
+
continue
|
|
71
|
+
if not isinstance(keys[kind], dict):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"{algorithm_name} '{kind}' must be a dict, "
|
|
74
|
+
f"got {type(keys[kind]).__name__}"
|
|
75
|
+
)
|
|
76
|
+
missing = [f for f in spec[kind] if f not in keys[kind]]
|
|
77
|
+
if missing:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"{algorithm_name} '{kind}' is missing required fields: {missing}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
nested = spec.get("nested") or {}
|
|
83
|
+
for parent, required in nested.items():
|
|
84
|
+
if "public_key" not in keys or parent not in keys["public_key"]:
|
|
85
|
+
continue
|
|
86
|
+
sub = keys["public_key"][parent]
|
|
87
|
+
if not isinstance(sub, dict):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"{algorithm_name} 'public_key.{parent}' must be a dict, "
|
|
90
|
+
f"got {type(sub).__name__}"
|
|
91
|
+
)
|
|
92
|
+
missing = [f for f in required if f not in sub]
|
|
93
|
+
if missing:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"{algorithm_name} 'public_key.{parent}' is missing required "
|
|
96
|
+
f"fields: {missing}"
|
|
97
|
+
)
|
|
@@ -15,6 +15,11 @@ logger = Logger(module="lightphe/cryptosystems/Benaloh.py")
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Benaloh(Homomorphic):
|
|
18
|
+
REQUIRED_KEYS = {
|
|
19
|
+
"public_key": ["y", "r", "n"],
|
|
20
|
+
"private_key": ["p", "q", "phi", "x"],
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
def __init__(
|
|
19
24
|
self,
|
|
20
25
|
keys: Optional[dict] = None,
|
|
@@ -5,6 +5,7 @@ from typing import Optional, Tuple, Union
|
|
|
5
5
|
|
|
6
6
|
# third-party dependencies
|
|
7
7
|
from lightecc import LightECC
|
|
8
|
+
from lightecc.commons.errors import InvalidCurveOrder, PointNotOnCurve
|
|
8
9
|
from lightecc.interfaces.elliptic_curve import EllipticCurvePoint
|
|
9
10
|
from lightecc.commons.pairing import _fp2_pow, _fp2_mul
|
|
10
11
|
import sympy
|
|
@@ -25,6 +26,12 @@ class BonehGohNissim(Homomorphic):
|
|
|
25
26
|
Ref: https://sefiks.com/2026/04/02/a-step-by-step-somewhat-homomorphic-encryption-example-with-boneh-goh-nissim-in-python/
|
|
26
27
|
"""
|
|
27
28
|
|
|
29
|
+
REQUIRED_KEYS = {
|
|
30
|
+
"public_key": ["curve", "G", "u", "h", "l"],
|
|
31
|
+
"private_key": ["q1", "q2"],
|
|
32
|
+
"nested": {"curve": ["a", "b", "p", "G", "n"]},
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
def __init__(
|
|
29
36
|
self,
|
|
30
37
|
keys: Optional[dict] = None,
|
|
@@ -106,22 +113,21 @@ class BonehGohNissim(Homomorphic):
|
|
|
106
113
|
|
|
107
114
|
G = self.__find_generator(p, max_tries=max_tries)
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
logger.debug("nG is the point at infinity, as expected.")
|
|
121
|
-
logger.debug(f"q1 = {q1}, q2 = {q2}, n = {n}, p = {p}, l = {l}")
|
|
122
|
-
break
|
|
116
|
+
try:
|
|
117
|
+
self.ec = LightECC(
|
|
118
|
+
form_name="weierstrass",
|
|
119
|
+
curve_name="custom",
|
|
120
|
+
config={"a": a, "b": b, "p": p, "G": G, "n": n},
|
|
121
|
+
)
|
|
122
|
+
except (InvalidCurveOrder, PointNotOnCurve):
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"Curve order validation failed for attempt {key_attempt + 1}, retrying"
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
123
127
|
|
|
124
|
-
logger.debug(
|
|
128
|
+
logger.debug("Curve order validated by LightECC.")
|
|
129
|
+
logger.debug(f"q1 = {q1}, q2 = {q2}, n = {n}, p = {p}, l = {l}")
|
|
130
|
+
break
|
|
125
131
|
else:
|
|
126
132
|
raise Exception(f"Failed to generate keys after {max_tries} attempts")
|
|
127
133
|
|
|
@@ -15,6 +15,11 @@ class DamgardJurik(Homomorphic):
|
|
|
15
15
|
Ref: https://sefiks.com/2023/10/20/a-step-by-step-partially-homomorphic-encryption-example-with-damgard-jurik-in-python/
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
REQUIRED_KEYS = {
|
|
19
|
+
"public_key": ["g", "n", "s"],
|
|
20
|
+
"private_key": ["phi"],
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
def __init__(self, s: int = 2, keys: Optional[dict] = None, key_size: Optional[int] = None):
|
|
19
24
|
"""
|
|
20
25
|
Args:
|
|
@@ -19,6 +19,11 @@ class ElGamal(Homomorphic):
|
|
|
19
19
|
Ref: https://sefiks.com/2023/03/27/a-step-by-step-partially-homomorphic-encryption-example-with-elgamal-in-python/
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
REQUIRED_KEYS = {
|
|
23
|
+
"public_key": ["p", "g", "y"],
|
|
24
|
+
"private_key": ["x"],
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
def __init__(self, keys: Optional[dict] = None, exponential=False, key_size: Optional[int] = None):
|
|
23
28
|
"""
|
|
24
29
|
Args:
|
|
@@ -22,6 +22,11 @@ class EllipticCurveElGamal(Homomorphic):
|
|
|
22
22
|
Ref: https://sefiks.com/2018/08/21/elliptic-curve-elgamal-encryption/
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
+
REQUIRED_KEYS = {
|
|
26
|
+
"public_key": ["Qa"],
|
|
27
|
+
"private_key": ["ka"],
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
def __init__(
|
|
26
31
|
self,
|
|
27
32
|
keys: Optional[dict] = None,
|
|
@@ -23,6 +23,11 @@ class GoldwasserMicali(Homomorphic):
|
|
|
23
23
|
Ref: sefiks.com/2023/10/27/a-step-by-step-partially-homomorphic-encryption-example-with-goldwasser-micali-in-python/
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
REQUIRED_KEYS = {
|
|
27
|
+
"public_key": ["n", "x"],
|
|
28
|
+
"private_key": ["p", "q"],
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
def __init__(
|
|
27
32
|
self,
|
|
28
33
|
keys: Optional[dict] = None,
|
|
@@ -20,6 +20,11 @@ class NaccacheStern(Homomorphic):
|
|
|
20
20
|
Original paper: https://dl.acm.org/doi/pdf/10.1145/288090.288106
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
REQUIRED_KEYS = {
|
|
24
|
+
"public_key": ["n", "g", "sigma"],
|
|
25
|
+
"private_key": ["a", "b", "p", "q", "phi"],
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
def __init__(
|
|
24
29
|
self,
|
|
25
30
|
keys: Optional[dict] = None,
|
|
@@ -54,10 +59,11 @@ class NaccacheStern(Homomorphic):
|
|
|
54
59
|
Returns:
|
|
55
60
|
keys (dict): containing 'private_key' and 'public_key'
|
|
56
61
|
"""
|
|
57
|
-
# Small
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
62
|
+
# Small primes are required so DLP remains solvable during decryption.
|
|
63
|
+
# Picking 4 out of these 8 keeps sigma small (<= ~23^4) and splits
|
|
64
|
+
# evenly into u and v.
|
|
65
|
+
small_primes = [3, 5, 7, 11, 13, 17, 19, 23]
|
|
66
|
+
prime_set = sorted(random.sample(small_primes, 4))
|
|
61
67
|
k = len(prime_set)
|
|
62
68
|
|
|
63
69
|
if all(sympy.isprime(prime) is True for prime in prime_set) is False:
|
|
@@ -167,7 +173,6 @@ class NaccacheStern(Homomorphic):
|
|
|
167
173
|
"p": p,
|
|
168
174
|
"q": q,
|
|
169
175
|
"phi": phi,
|
|
170
|
-
"prime_set": prime_set,
|
|
171
176
|
},
|
|
172
177
|
}
|
|
173
178
|
logger.debug(
|
|
@@ -227,7 +232,9 @@ class NaccacheStern(Homomorphic):
|
|
|
227
232
|
phi = self.keys["private_key"]["phi"]
|
|
228
233
|
n = self.keys["public_key"]["n"]
|
|
229
234
|
g = self.keys["public_key"]["g"]
|
|
230
|
-
|
|
235
|
+
sigma = self.keys["public_key"]["sigma"]
|
|
236
|
+
# sigma is the product of the small primes; recover them by factoring.
|
|
237
|
+
prime_set = sorted(sympy.factorint(sigma).keys())
|
|
231
238
|
|
|
232
239
|
remainders = []
|
|
233
240
|
for i, prime in enumerate(prime_set):
|
|
@@ -14,6 +14,11 @@ class OkamotoUchiyama(Homomorphic):
|
|
|
14
14
|
Ref: https://sefiks.com/2023/10/20/a-step-by-step-partially-homomorphic-encryption-example-with-okamoto-uchiyama-in-python/
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
REQUIRED_KEYS = {
|
|
18
|
+
"public_key": ["n", "g", "h"],
|
|
19
|
+
"private_key": ["p", "q"],
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
def __init__(self, keys: Optional[dict] = None, key_size: Optional[int] = None):
|
|
18
23
|
"""
|
|
19
24
|
Args:
|
|
@@ -15,6 +15,11 @@ class Paillier(Homomorphic):
|
|
|
15
15
|
Ref: https://sefiks.com/2023/04/03/a-step-by-step-partially-homomorphic-encryption-example-with-paillier-in-python/
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
REQUIRED_KEYS = {
|
|
19
|
+
"public_key": ["g", "n"],
|
|
20
|
+
"private_key": ["phi"],
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
def __init__(self, keys: Optional[dict] = None, key_size: Optional[int] = None):
|
|
19
24
|
"""
|
|
20
25
|
Args:
|
|
@@ -14,6 +14,11 @@ class RSA(Homomorphic):
|
|
|
14
14
|
Ref: https://sefiks.com/2023/03/06/a-step-by-step-partially-homomorphic-encryption-example-with-rsa-in-python/
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
REQUIRED_KEYS = {
|
|
18
|
+
"public_key": ["n", "e"],
|
|
19
|
+
"private_key": ["d"],
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
def __init__(
|
|
18
23
|
self,
|
|
19
24
|
keys: Optional[dict] = None,
|
|
@@ -21,6 +21,11 @@ class SanderYoungYung(Homomorphic):
|
|
|
21
21
|
Ref: https://sefiks.com/2026/04/02/a-step-by-step-partially-homomorphic-sander-young-yung-example-in-python/
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
REQUIRED_KEYS = {
|
|
25
|
+
"public_key": ["n", "x", "l"],
|
|
26
|
+
"private_key": ["p", "q"],
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
def __init__(
|
|
25
30
|
self,
|
|
26
31
|
keys: Optional[dict] = None,
|
|
@@ -17,6 +17,13 @@ class Homomorphic(ABC):
|
|
|
17
17
|
plaintext_modulo: int
|
|
18
18
|
ciphertext_modulo: int
|
|
19
19
|
|
|
20
|
+
# Cryptosystems must define REQUIRED_KEYS as a dict with:
|
|
21
|
+
# - "public_key": list of required sub-fields in keys["public_key"]
|
|
22
|
+
# - "private_key": list of required sub-fields in keys["private_key"]
|
|
23
|
+
# - "nested" (optional): dict mapping a public_key sub-field that itself
|
|
24
|
+
# holds a dict to its required sub-fields.
|
|
25
|
+
REQUIRED_KEYS: dict
|
|
26
|
+
|
|
20
27
|
@abstractmethod
|
|
21
28
|
def generate_keys(
|
|
22
29
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lightphe
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.25
|
|
4
4
|
Summary: A Lightweight Partially Homomorphic Encryption Library for Python
|
|
5
5
|
Home-page: https://github.com/serengil/LightPHE
|
|
6
6
|
Author: Sefik Ilkin Serengil
|
|
@@ -65,7 +65,7 @@ from lightphe import LightPHE
|
|
|
65
65
|
|
|
66
66
|
# Summary of Homomorphic Features of Different Cryptosystems in LightPHE
|
|
67
67
|
|
|
68
|
-
In summary, LightPHE is covering following algorithms and these are partially homomorphic with respect to the operations mentioned in the following table.
|
|
68
|
+
In summary, LightPHE is covering following algorithms and these are partially homomorphic and somewhat homomorphic with respect to the operations mentioned in the following table.
|
|
69
69
|
|
|
70
70
|
| Algorithm | Multiplicatively<br>Homomorphic | Additively<br>Homomorphic | Scalar Multiplication | Bitwise-XOR Homomorphic | Bitwise-AND Homomorphic | Regeneration<br>of Ciphertext |
|
|
71
71
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -156,7 +156,7 @@ assert cs.decrypt(c3) == m1 * m2
|
|
|
156
156
|
|
|
157
157
|
### Ciphertext Regeneration
|
|
158
158
|
|
|
159
|
-
The most of
|
|
159
|
+
The most of homomorphic algorithms allow you to regenerate ciphertext while you are not breaking its plaintext restoration. You may consider to do this re-generation many times to have stronger ciphertexts.
|
|
160
160
|
|
|
161
161
|
```python
|
|
162
162
|
c1_prime = cs.regenerate_ciphertext(c1)
|
|
@@ -248,22 +248,17 @@ Also, your company's logo will be shown on README on GitHub if you become sponso
|
|
|
248
248
|
Please cite LightPHE in your publications if it helps your research. Here is its BibTex entry:
|
|
249
249
|
|
|
250
250
|
```BibTeX
|
|
251
|
-
@article{
|
|
252
|
-
title={
|
|
253
|
-
author={
|
|
254
|
-
journal={
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
title={Encrypted Vector Similarity Computations Using Partially Homomorphic Encryption: Applications and Performance Analysis},
|
|
263
|
-
author={Serengil, Sefik and Ozpinar, Alper},
|
|
264
|
-
journal={arXiv preprint arXiv:2503.05850},
|
|
265
|
-
note={doi: 10.48550/arXiv.2503.05850. [Online]. Available: \url{https://arxiv.org/abs/2503.05850}},
|
|
266
|
-
year={2025}
|
|
251
|
+
@article{sym18050832,
|
|
252
|
+
title = {Sustainable Cryptography: Carbon Asymmetry in Partially Homomorphic Encryption in the Cloud},
|
|
253
|
+
author = {Ozpinar, Alper and Serengil, Sefik Ilkin},
|
|
254
|
+
journal = {Symmetry},
|
|
255
|
+
volume = {18},
|
|
256
|
+
number = {5},
|
|
257
|
+
pages = {832},
|
|
258
|
+
url = {https://www.mdpi.com/2073-8994/18/5/832},
|
|
259
|
+
doi = {10.3390/sym18050832},
|
|
260
|
+
note = {Special Issue: Symmetry in Cryptography and Cybersecurity},
|
|
261
|
+
year = {2026}
|
|
267
262
|
}
|
|
268
263
|
```
|
|
269
264
|
|
|
@@ -9,6 +9,7 @@ lightphe.egg-info/dependency_links.txt
|
|
|
9
9
|
lightphe.egg-info/requires.txt
|
|
10
10
|
lightphe.egg-info/top_level.txt
|
|
11
11
|
lightphe/commons/__init__.py
|
|
12
|
+
lightphe/commons/key_validator.py
|
|
12
13
|
lightphe/commons/logger.py
|
|
13
14
|
lightphe/commons/phe_utils.py
|
|
14
15
|
lightphe/cryptosystems/Benaloh.py
|
|
@@ -38,6 +39,7 @@ tests/test_elgamal.py
|
|
|
38
39
|
tests/test_ellipticcurveelgamal.py
|
|
39
40
|
tests/test_exporting_keys.py
|
|
40
41
|
tests/test_goldwasser.py
|
|
42
|
+
tests/test_key_validator.py
|
|
41
43
|
tests/test_keys.py
|
|
42
44
|
tests/test_large_keys.py
|
|
43
45
|
tests/test_naccache.py
|
|
@@ -8,7 +8,7 @@ with open("requirements.txt", "r", encoding="utf-8") as f:
|
|
|
8
8
|
|
|
9
9
|
setuptools.setup(
|
|
10
10
|
name="lightphe",
|
|
11
|
-
version="0.0.
|
|
11
|
+
version="0.0.25",
|
|
12
12
|
author="Sefik Ilkin Serengil",
|
|
13
13
|
author_email="serengil@gmail.com",
|
|
14
14
|
description="A Lightweight Partially Homomorphic Encryption Library for Python",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from lightphe import LightPHE
|
|
6
|
+
from lightphe.commons.logger import Logger
|
|
7
|
+
|
|
8
|
+
logger = Logger(module="tests/test_key_validator.py")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ALGORITHMS = [
|
|
12
|
+
("RSA", 50),
|
|
13
|
+
("ElGamal", 50),
|
|
14
|
+
("Exponential-ElGamal", 50),
|
|
15
|
+
("EllipticCurve-ElGamal", 50),
|
|
16
|
+
("Paillier", 50),
|
|
17
|
+
("Damgard-Jurik", 50),
|
|
18
|
+
("Okamoto-Uchiyama", 50),
|
|
19
|
+
("Benaloh", 50),
|
|
20
|
+
("Naccache-Stern", 50),
|
|
21
|
+
("Goldwasser-Micali", 50),
|
|
22
|
+
("Sander-Young-Yung", 50),
|
|
23
|
+
("Boneh-Goh-Nissim", 50),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize("algorithm_name,key_size", ALGORITHMS)
|
|
28
|
+
def test_missing_public_key_field_is_rejected(algorithm_name, key_size):
|
|
29
|
+
"""Dropping a required public_key sub-field must raise ValueError."""
|
|
30
|
+
cs = LightPHE(algorithm_name=algorithm_name, key_size=key_size, max_tries=10000)
|
|
31
|
+
field = cs.cs.REQUIRED_KEYS["public_key"][0]
|
|
32
|
+
|
|
33
|
+
broken = copy.deepcopy(cs.cs.keys)
|
|
34
|
+
del broken["public_key"][field]
|
|
35
|
+
|
|
36
|
+
with pytest.raises(ValueError, match=f"missing required fields.*{field}"):
|
|
37
|
+
LightPHE(algorithm_name=algorithm_name, keys=broken)
|
|
38
|
+
|
|
39
|
+
logger.info(f"✅ {algorithm_name}: missing public_key.{field} rejected")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("algorithm_name,key_size", ALGORITHMS)
|
|
43
|
+
def test_missing_private_key_field_is_rejected(algorithm_name, key_size):
|
|
44
|
+
"""Dropping a required private_key sub-field must raise ValueError."""
|
|
45
|
+
cs = LightPHE(algorithm_name=algorithm_name, key_size=key_size, max_tries=10000)
|
|
46
|
+
field = cs.cs.REQUIRED_KEYS["private_key"][0]
|
|
47
|
+
|
|
48
|
+
broken = copy.deepcopy(cs.cs.keys)
|
|
49
|
+
del broken["private_key"][field]
|
|
50
|
+
|
|
51
|
+
with pytest.raises(ValueError, match=f"missing required fields.*{field}"):
|
|
52
|
+
LightPHE(algorithm_name=algorithm_name, keys=broken)
|
|
53
|
+
|
|
54
|
+
logger.info(f"✅ {algorithm_name}: missing private_key.{field} rejected")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_missing_public_key_dict_is_rejected():
|
|
58
|
+
"""A keys dict without public_key must be rejected."""
|
|
59
|
+
with pytest.raises(ValueError, match="must contain 'public_key'"):
|
|
60
|
+
LightPHE(algorithm_name="RSA", keys={})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_private_key_only_is_rejected():
|
|
64
|
+
"""Supplying private_key without public_key must be rejected."""
|
|
65
|
+
cs = LightPHE(algorithm_name="RSA", key_size=50)
|
|
66
|
+
private_only = {"private_key": copy.deepcopy(cs.cs.keys["private_key"])}
|
|
67
|
+
with pytest.raises(ValueError, match="must contain 'public_key'"):
|
|
68
|
+
LightPHE(algorithm_name="RSA", keys=private_only)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_non_dict_keys_is_rejected():
|
|
72
|
+
"""A non-dict `keys` argument must be rejected."""
|
|
73
|
+
with pytest.raises(ValueError, match="keys must be a dict"):
|
|
74
|
+
LightPHE(algorithm_name="RSA", keys="not-a-dict")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_public_key_only_is_accepted():
|
|
78
|
+
"""Supplying only a full public_key must be accepted (encryption-only use)."""
|
|
79
|
+
cs = LightPHE(algorithm_name="RSA", key_size=50)
|
|
80
|
+
public_only = {"public_key": copy.deepcopy(cs.cs.keys["public_key"])}
|
|
81
|
+
# should not raise
|
|
82
|
+
LightPHE(algorithm_name="RSA", keys=public_only)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_bgn_nested_curve_field_is_rejected():
|
|
86
|
+
"""Dropping a required BGN curve sub-field must be rejected."""
|
|
87
|
+
cs = LightPHE(algorithm_name="Boneh-Goh-Nissim", key_size=50, max_tries=10000)
|
|
88
|
+
nested = cs.cs.REQUIRED_KEYS.get("nested", {}).get("curve", [])
|
|
89
|
+
assert nested, "BGN must declare nested curve required fields"
|
|
90
|
+
field = nested[0]
|
|
91
|
+
|
|
92
|
+
broken = copy.deepcopy(cs.cs.keys)
|
|
93
|
+
del broken["public_key"]["curve"][field]
|
|
94
|
+
|
|
95
|
+
with pytest.raises(
|
|
96
|
+
ValueError, match=rf"public_key\.curve.*missing required fields.*{field}"
|
|
97
|
+
):
|
|
98
|
+
LightPHE(algorithm_name="Boneh-Goh-Nissim", keys=broken)
|
|
99
|
+
|
|
100
|
+
logger.info(f"✅ BGN: missing public_key.curve.{field} rejected")
|
|
@@ -23,7 +23,6 @@ def test_predefined_keys():
|
|
|
23
23
|
"p": 2435378721695886057509882054966317293278393717736720131443144187793510457493213363945300094351007614623267282257550830261688773235268185768920658432760349591,
|
|
24
24
|
"q": 30909025587173888031151320439694649143770740813345918276468695473548278783448460260839705132326515527669756851362284434022860932531472563869828240743607816519,
|
|
25
25
|
"phi": 75275183223356977395233015128636801841182684353869611205877871399639096259927846711964449336873721703958210179653876541687516247046310691985977881277598653806134265954388527744361895141375770783671899073915178636812972605279037227887538978553609742276161772286758627935090233305041395360684118422229078186256527620,
|
|
26
|
-
"prime_set": [3, 5, 7, 11, 13, 17],
|
|
27
26
|
},
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -33,7 +32,7 @@ def test_predefined_keys():
|
|
|
33
32
|
|
|
34
33
|
security_level = cs.cs.keys["public_key"]["n"].bit_length()
|
|
35
34
|
|
|
36
|
-
logger.
|
|
35
|
+
logger.debug(
|
|
37
36
|
f"Naccache-Stern cs built with {security_level} bits key. Recommended >= 768."
|
|
38
37
|
)
|
|
39
38
|
|
|
@@ -44,13 +43,13 @@ def test_predefined_keys():
|
|
|
44
43
|
c1 = cs.encrypt(plaintext=m1)
|
|
45
44
|
c2 = cs.encrypt(plaintext=m2)
|
|
46
45
|
toc = time.time()
|
|
47
|
-
logger.
|
|
46
|
+
logger.debug(f"Naccache-Stern encryption time: {(toc - tic)/2:.4f} seconds")
|
|
48
47
|
|
|
49
48
|
# homomorphic addition
|
|
50
49
|
tic = time.time()
|
|
51
50
|
assert cs.decrypt(c1 + c2) == m1 + m2
|
|
52
51
|
toc = time.time()
|
|
53
|
-
logger.
|
|
52
|
+
logger.debug(f"Naccache-Stern homomorphic addition time: {(toc - tic):.4f} seconds")
|
|
54
53
|
|
|
55
54
|
# homomorphic scalar multiplication
|
|
56
55
|
tic = time.time()
|
|
@@ -59,7 +58,7 @@ def test_predefined_keys():
|
|
|
59
58
|
m2_times_c1 = m2 * c1
|
|
60
59
|
m1_times_c2 = m1 * c2
|
|
61
60
|
toc = time.time()
|
|
62
|
-
logger.
|
|
61
|
+
logger.debug(
|
|
63
62
|
f"Naccache-Stern homomorphic scalar multiplication time: {(toc - tic)/4:.4f} seconds"
|
|
64
63
|
)
|
|
65
64
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|