lightphe 0.0.15__tar.gz → 0.0.16__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.15 → lightphe-0.0.16}/PKG-INFO +11 -5
- {lightphe-0.0.15 → lightphe-0.0.16}/README.md +10 -4
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/__init__.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/Benaloh.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/DamgardJurik.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/ElGamal.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/EllipticCurveElGamal.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/GoldwasserMicali.py +33 -30
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/NaccacheStern.py +103 -94
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/OkamotoUchiyama.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/Paillier.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Ciphertext.py +2 -2
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Homomorphic.py +21 -7
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Tensor.py +2 -2
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/PKG-INFO +11 -5
- lightphe-0.0.16/lightphe.egg-info/requires.txt +4 -0
- lightphe-0.0.16/requirements.txt +4 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/setup.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_benaloh.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_damgard.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_elgamal.py +2 -2
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_ellipticcurveelgamal.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_goldwasser.py +10 -3
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_keys.py +7 -2
- lightphe-0.0.16/tests/test_naccache.py +203 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_okamoto.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_paillier.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_rsa.py +1 -1
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_salary.py +1 -1
- lightphe-0.0.15/lightphe.egg-info/requires.txt +0 -4
- lightphe-0.0.15/requirements.txt +0 -4
- lightphe-0.0.15/tests/test_naccache.py +0 -94
- {lightphe-0.0.15 → lightphe-0.0.16}/LICENSE +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/commons/__init__.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/commons/logger.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/commons/phe_utils.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/RSA.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/__init__.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Algorithm.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/__init__.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/SOURCES.txt +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/dependency_links.txt +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/top_level.txt +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/setup.cfg +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/__init__.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_ciphertexts.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_cloud.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_exporting_keys.py +0 -0
- {lightphe-0.0.15 → lightphe-0.0.16}/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.16
|
|
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
|
|
@@ -28,7 +28,7 @@ License-File: LICENSE
|
|
|
28
28
|
[](https://www.youtube.com/@sefiks?sub_confirmation=1)
|
|
29
29
|
[](https://twitter.com/intent/user?screen_name=serengil)
|
|
30
30
|
|
|
31
|
-
[](https://www.patreon.com/serengil?repo=lightphe)
|
|
32
32
|
[](https://github.com/sponsors/serengil)
|
|
33
33
|
[](https://buymeacoffee.com/serengil)
|
|
34
34
|
|
|
@@ -262,7 +262,7 @@ One of the crucial factors that define the security level of an elliptic curve c
|
|
|
262
262
|
|
|
263
263
|
Each curve in LightPHE has a specific order, which is carefully chosen to balance performance and security. By selecting an elliptic curve with a larger order, you increase the security of your cryptographic system, but this may come with a trade-off in computational efficiency. Therefore, choosing the appropriate curve order is a crucial decision based on your application’s security and performance requirements.
|
|
264
264
|
|
|
265
|
-
See [`curves`](https://github.com/serengil/LightECC) page for a list of all supported forms, curves and their details.
|
|
265
|
+
See [`curves`](https://github.com/serengil/LightECC#supported-curves) page for a list of all supported forms, curves and their details.
|
|
266
266
|
|
|
267
267
|
### Vector Embeddings and Tensors
|
|
268
268
|
|
|
@@ -379,6 +379,12 @@ calculated_similarity = cs.decrypt(encrypted_cosine_similarity)[0]
|
|
|
379
379
|
assert abs(calculated_similarity - expected_similarity) < 1e-2
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
+
## Security Concerns and Considerations
|
|
383
|
+
|
|
384
|
+
While LightPHE enables encryption using public keys and supports homomorphic operations on encrypted data, it's important to understand a subtle but critical security implication of this approach. Since the it is homomorphic, the cloud (or any third-party processor) can perform computations without knowing the plaintext. However, a malicious actor can still encrypt arbitrary values and craft valid ciphertexts because encryption depends on your public key and it is publicly known. For example, even if a salary is encrypted, an attacker could encrypt 500 USD using your public key and update the encrypted salary with this forged ciphertext — without needing to know the original value.
|
|
385
|
+
|
|
386
|
+
To mitigate this, we recommend combining LightPHE with digital signature schemes. By keeping a signed audit trail of update operations in a separate log table, you can detect and reject unauthorized changes, and even restore the original data in case of tampering. We suggest checking out [`LightDSA`](https://github.com/serengil/LightDSA), which provides a lightweight cryptographic interface similar to LightPHE and supports popular digital signature algorithms like RSA, DSA, ECDSA, and EdDSA.
|
|
387
|
+
|
|
382
388
|
# Contributing
|
|
383
389
|
|
|
384
390
|
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.
|
|
@@ -392,11 +398,11 @@ There are many ways to support a project - starring⭐️ the GitHub repo is jus
|
|
|
392
398
|
You can also support this work on [Patreon](https://www.patreon.com/serengil?repo=lightphe), [GitHub Sponsors](https://github.com/sponsors/serengil) or [Buy Me a Coffee](https://buymeacoffee.com/serengil).
|
|
393
399
|
|
|
394
400
|
<a href="https://www.patreon.com/serengil?repo=lightphe">
|
|
395
|
-
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/patreon.png" width="30%"
|
|
401
|
+
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/patreon.png" width="30%">
|
|
396
402
|
</a>
|
|
397
403
|
|
|
398
404
|
<a href="https://buymeacoffee.com/serengil">
|
|
399
|
-
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/bmc-button.png" width="25%"
|
|
405
|
+
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/bmc-button.png" width="25%">
|
|
400
406
|
</a>
|
|
401
407
|
|
|
402
408
|
Also, your company's logo will be shown on README on GitHub if you become sponsor in gold, silver or bronze tiers.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.youtube.com/@sefiks?sub_confirmation=1)
|
|
13
13
|
[](https://twitter.com/intent/user?screen_name=serengil)
|
|
14
14
|
|
|
15
|
-
[](https://www.patreon.com/serengil?repo=lightphe)
|
|
16
16
|
[](https://github.com/sponsors/serengil)
|
|
17
17
|
[](https://buymeacoffee.com/serengil)
|
|
18
18
|
|
|
@@ -246,7 +246,7 @@ One of the crucial factors that define the security level of an elliptic curve c
|
|
|
246
246
|
|
|
247
247
|
Each curve in LightPHE has a specific order, which is carefully chosen to balance performance and security. By selecting an elliptic curve with a larger order, you increase the security of your cryptographic system, but this may come with a trade-off in computational efficiency. Therefore, choosing the appropriate curve order is a crucial decision based on your application’s security and performance requirements.
|
|
248
248
|
|
|
249
|
-
See [`curves`](https://github.com/serengil/LightECC) page for a list of all supported forms, curves and their details.
|
|
249
|
+
See [`curves`](https://github.com/serengil/LightECC#supported-curves) page for a list of all supported forms, curves and their details.
|
|
250
250
|
|
|
251
251
|
### Vector Embeddings and Tensors
|
|
252
252
|
|
|
@@ -363,6 +363,12 @@ calculated_similarity = cs.decrypt(encrypted_cosine_similarity)[0]
|
|
|
363
363
|
assert abs(calculated_similarity - expected_similarity) < 1e-2
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
## Security Concerns and Considerations
|
|
367
|
+
|
|
368
|
+
While LightPHE enables encryption using public keys and supports homomorphic operations on encrypted data, it's important to understand a subtle but critical security implication of this approach. Since the it is homomorphic, the cloud (or any third-party processor) can perform computations without knowing the plaintext. However, a malicious actor can still encrypt arbitrary values and craft valid ciphertexts because encryption depends on your public key and it is publicly known. For example, even if a salary is encrypted, an attacker could encrypt 500 USD using your public key and update the encrypted salary with this forged ciphertext — without needing to know the original value.
|
|
369
|
+
|
|
370
|
+
To mitigate this, we recommend combining LightPHE with digital signature schemes. By keeping a signed audit trail of update operations in a separate log table, you can detect and reject unauthorized changes, and even restore the original data in case of tampering. We suggest checking out [`LightDSA`](https://github.com/serengil/LightDSA), which provides a lightweight cryptographic interface similar to LightPHE and supports popular digital signature algorithms like RSA, DSA, ECDSA, and EdDSA.
|
|
371
|
+
|
|
366
372
|
# Contributing
|
|
367
373
|
|
|
368
374
|
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.
|
|
@@ -376,11 +382,11 @@ There are many ways to support a project - starring⭐️ the GitHub repo is jus
|
|
|
376
382
|
You can also support this work on [Patreon](https://www.patreon.com/serengil?repo=lightphe), [GitHub Sponsors](https://github.com/sponsors/serengil) or [Buy Me a Coffee](https://buymeacoffee.com/serengil).
|
|
377
383
|
|
|
378
384
|
<a href="https://www.patreon.com/serengil?repo=lightphe">
|
|
379
|
-
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/patreon.png" width="30%"
|
|
385
|
+
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/patreon.png" width="30%">
|
|
380
386
|
</a>
|
|
381
387
|
|
|
382
388
|
<a href="https://buymeacoffee.com/serengil">
|
|
383
|
-
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/bmc-button.png" width="25%"
|
|
389
|
+
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/bmc-button.png" width="25%">
|
|
384
390
|
</a>
|
|
385
391
|
|
|
386
392
|
Also, your company's logo will be shown on README on GitHub if you become sponsor in gold, silver or bronze tiers.
|
|
@@ -169,7 +169,7 @@ class Benaloh(Homomorphic):
|
|
|
169
169
|
n = self.keys["public_key"]["n"]
|
|
170
170
|
return (ciphertext1 * ciphertext2) % n
|
|
171
171
|
|
|
172
|
-
def
|
|
172
|
+
def multiply_by_constant(self, ciphertext: int, constant: int) -> int:
|
|
173
173
|
"""
|
|
174
174
|
Multiply a ciphertext with a plain constant.
|
|
175
175
|
Result of this must be equal to E(m1 * constant) where E(m1) = ciphertext
|
|
@@ -125,7 +125,7 @@ class DamgardJurik(Homomorphic):
|
|
|
125
125
|
modulo = pow(n, s + 1)
|
|
126
126
|
return (ciphertext1 * ciphertext2) % modulo
|
|
127
127
|
|
|
128
|
-
def
|
|
128
|
+
def multiply_by_constant(self, ciphertext: int, constant: int) -> int:
|
|
129
129
|
"""
|
|
130
130
|
Multiply a ciphertext by a known plain constant
|
|
131
131
|
Result of this must be equal to E(m1 * m2), where E(m1) = ciphertext
|
|
@@ -169,7 +169,7 @@ class ElGamal(Homomorphic):
|
|
|
169
169
|
p = self.keys["public_key"]["p"]
|
|
170
170
|
return (ciphertext1[0] * ciphertext2[0]) % p, (ciphertext1[1] * ciphertext2[1]) % p
|
|
171
171
|
|
|
172
|
-
def
|
|
172
|
+
def multiply_by_constant(self, ciphertext: tuple, constant: int) -> tuple:
|
|
173
173
|
if self.exponential is False:
|
|
174
174
|
raise ValueError("ElGamal is not supporting multiplying ciphertext by a known constant")
|
|
175
175
|
p = self.keys["public_key"]["p"]
|
|
@@ -168,7 +168,7 @@ class EllipticCurveElGamal(Homomorphic):
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
def
|
|
171
|
+
def multiply_by_constant(self, ciphertext: tuple, constant: int) -> tuple:
|
|
172
172
|
"""
|
|
173
173
|
Multiply a ciphertext with a plain constant.
|
|
174
174
|
Result of this must be equal to k x E(m1) = E(m1 * k)
|
|
@@ -6,6 +6,7 @@ import math
|
|
|
6
6
|
# 3rd party dependencies
|
|
7
7
|
import sympy
|
|
8
8
|
from sympy import jacobi_symbol
|
|
9
|
+
from tqdm import tqdm
|
|
9
10
|
|
|
10
11
|
# project dependencies
|
|
11
12
|
from lightphe.models.Homomorphic import Homomorphic
|
|
@@ -29,51 +30,53 @@ class GoldwasserMicali(Homomorphic):
|
|
|
29
30
|
set this to None if you want to generate random keys.
|
|
30
31
|
key_size (int): key size in bits
|
|
31
32
|
"""
|
|
32
|
-
|
|
33
|
-
self.keys = keys or self.generate_keys(key_size or
|
|
33
|
+
|
|
34
|
+
self.keys = keys or self.generate_keys(key_size or 1024)
|
|
34
35
|
self.ciphertext_modulo = self.keys["public_key"]["n"]
|
|
35
36
|
# TODO: not sure about the plaintext modulo
|
|
36
37
|
self.plaintext_modulo = self.keys["public_key"]["n"]
|
|
37
38
|
|
|
38
|
-
def generate_keys(self, key_size: int) -> dict:
|
|
39
|
+
def generate_keys(self, key_size: int, max_tries: int = 10000) -> dict:
|
|
39
40
|
"""
|
|
40
41
|
Generate public and private keys of Goldwasser-Micali cryptosystem
|
|
41
42
|
Args:
|
|
42
43
|
key_size (int): key size in bits
|
|
44
|
+
max_tries (int): maximum number of tries to generate keys
|
|
43
45
|
Returns:
|
|
44
46
|
keys (dict): having private_key and public_key keys
|
|
45
47
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
for attempt in tqdm(range(max_tries), disable=True):
|
|
49
|
+
# pick large random primes p and q
|
|
50
|
+
p = sympy.randprime(2 ** (key_size // 2 - 100), 2 ** (key_size // 2) - 1)
|
|
51
|
+
q = sympy.randprime(2 ** (key_size // 2 - 100), 2 ** (key_size // 2) - 1)
|
|
52
|
+
|
|
53
|
+
# to prevent factorizatin attacks, it is recommended that n should be
|
|
54
|
+
# several hundred bits or more
|
|
55
|
+
n = p * q
|
|
56
|
+
|
|
57
|
+
# find quadratic non-residue x
|
|
58
|
+
for _ in range(1000): # try max 1000 random x
|
|
59
|
+
x = random.randint(2, n - 1)
|
|
60
|
+
if math.gcd(x, n) != 1:
|
|
61
|
+
continue
|
|
62
|
+
if jacobi_symbol(x, p) == -1 and jacobi_symbol(x, q) == -1:
|
|
63
|
+
break
|
|
64
|
+
else:
|
|
65
|
+
# # If no suitable x is found after 1000 tries, discard the current p–q pair and retry
|
|
66
|
+
continue
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
math.gcd(x, n) == 1
|
|
65
|
-
and jacobi_symbol(x, p) == -1
|
|
66
|
-
and jacobi_symbol(x, q) == -1
|
|
67
|
-
):
|
|
68
|
-
break
|
|
68
|
+
keys = {
|
|
69
|
+
"public_key": {"n": n, "x": x},
|
|
70
|
+
"private_key": {"p": p, "q": q},
|
|
71
|
+
}
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
logger.debug(
|
|
74
|
+
f"Goldwasser-Micali keys generated after {attempt+1} attempts, n bits: {n.bit_length()}"
|
|
75
|
+
)
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
keys["private_key"]["q"] = q
|
|
77
|
+
return keys
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
raise RuntimeError(f"Failed to generate keys after {max_tries} attempts")
|
|
77
80
|
|
|
78
81
|
def generate_random_key(self) -> int:
|
|
79
82
|
"""
|
|
@@ -3,12 +3,13 @@ from typing import Optional
|
|
|
3
3
|
import math
|
|
4
4
|
import sympy
|
|
5
5
|
from sympy.ntheory.modular import solve_congruence
|
|
6
|
+
from tqdm import tqdm
|
|
6
7
|
from lightphe.models.Homomorphic import Homomorphic
|
|
7
8
|
from lightphe.commons.logger import Logger
|
|
8
9
|
|
|
9
10
|
logger = Logger(module="lightphe/cryptosystems/NaccacheStern.py")
|
|
10
11
|
|
|
11
|
-
# pylint: disable=simplifiable-if-expression, consider-using-enumerate
|
|
12
|
+
# pylint: disable=simplifiable-if-expression, consider-using-enumerate, pointless-string-statement
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class NaccacheStern(Homomorphic):
|
|
@@ -19,7 +20,12 @@ class NaccacheStern(Homomorphic):
|
|
|
19
20
|
Original paper: https://dl.acm.org/doi/pdf/10.1145/288090.288106
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
|
-
def __init__(
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
keys: Optional[dict] = None,
|
|
26
|
+
key_size: Optional[int] = None,
|
|
27
|
+
deterministic: bool = False,
|
|
28
|
+
):
|
|
23
29
|
"""
|
|
24
30
|
Args:
|
|
25
31
|
keys (dict): private - public key pair.
|
|
@@ -30,26 +36,24 @@ class NaccacheStern(Homomorphic):
|
|
|
30
36
|
cryptosystem
|
|
31
37
|
"""
|
|
32
38
|
# Naccache-Stern requires to solve DLP in decryption, so small key is recommended
|
|
33
|
-
self.keys = keys or self.generate_keys(key_size or
|
|
39
|
+
self.keys = keys or self.generate_keys(key_size or 1024)
|
|
34
40
|
self.plaintext_modulo = self.keys["public_key"]["sigma"]
|
|
35
41
|
self.ciphertext_modulo = self.keys["public_key"]["n"]
|
|
36
42
|
self.deterministic = deterministic
|
|
37
43
|
|
|
38
|
-
def generate_keys(self, key_size: int) -> dict:
|
|
44
|
+
def generate_keys(self, key_size: int, max_tries: int = 10000) -> dict:
|
|
39
45
|
"""
|
|
40
46
|
Generate public and private keys of Naccache-Stern cryptosystem
|
|
41
47
|
Args:
|
|
42
|
-
key_size (int): key size in bits
|
|
48
|
+
key_size (int): key size in bits (≥1024 recommended)
|
|
49
|
+
max_tries (int): maximum attempts to generate keys
|
|
43
50
|
Returns:
|
|
44
|
-
keys (dict):
|
|
51
|
+
keys (dict): containing 'private_key' and 'public_key'
|
|
45
52
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# pick a family of small primes. the largest one is 10-bits
|
|
51
|
-
# TODO: do something generic instead of constant primes
|
|
52
|
-
prime_set = [3, 5, 7, 11, 13, 17]
|
|
53
|
+
# Small prime set (the largest one is 10-bits)
|
|
54
|
+
# Using a smaller prime_set to make key generation feasible for large key sizes
|
|
55
|
+
# prime_set = [3, 5, 7, 11, 13, 17]
|
|
56
|
+
prime_set = [3, 5, 7, 11]
|
|
53
57
|
k = len(prime_set)
|
|
54
58
|
|
|
55
59
|
if all(sympy.isprime(prime) is True for prime in prime_set) is False:
|
|
@@ -58,24 +62,30 @@ class NaccacheStern(Homomorphic):
|
|
|
58
62
|
# divide the set in half and find products of primes
|
|
59
63
|
u = 1
|
|
60
64
|
v = 1
|
|
61
|
-
|
|
62
65
|
for i, prime in enumerate(prime_set):
|
|
63
|
-
if i <
|
|
64
|
-
u
|
|
66
|
+
if i < k // 2:
|
|
67
|
+
u *= prime
|
|
65
68
|
else:
|
|
66
|
-
v
|
|
69
|
+
v *= prime
|
|
67
70
|
|
|
68
71
|
# product of all primes
|
|
69
72
|
sigma = u * v
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
for attempt in tqdm(
|
|
75
|
+
range(max_tries),
|
|
76
|
+
desc="Attempting Naccache-Stern key generation",
|
|
77
|
+
disable=True,
|
|
78
|
+
):
|
|
79
|
+
# Generate large random primes a and b
|
|
80
|
+
a = sympy.randprime(2 ** (key_size // 2 - 300), 2 ** (key_size // 2) - 1)
|
|
81
|
+
b = sympy.randprime(2 ** (key_size // 2 - 300), 2 ** (key_size // 2) - 1)
|
|
75
82
|
|
|
76
83
|
# calculate two primes from chosen ones
|
|
77
|
-
p =
|
|
78
|
-
q =
|
|
84
|
+
p = 2 * a * u + 1
|
|
85
|
+
q = 2 * b * v + 1
|
|
86
|
+
|
|
87
|
+
if not (sympy.isprime(p) and sympy.isprime(q)):
|
|
88
|
+
continue
|
|
79
89
|
|
|
80
90
|
# recommended n is 768 bits
|
|
81
91
|
n = p * q
|
|
@@ -89,83 +99,82 @@ class NaccacheStern(Homomorphic):
|
|
|
89
99
|
logger.debug("canceled because sigma and phi/sigma are not coprime")
|
|
90
100
|
continue
|
|
91
101
|
|
|
92
|
-
p_conditions
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
# p_conditions and q_conditions check omitted for practical large-key generation
|
|
103
|
+
if key_size < 40:
|
|
104
|
+
p_conditions = []
|
|
105
|
+
for i in range(0, int(k / 2)):
|
|
106
|
+
pi = prime_set[i]
|
|
107
|
+
if (
|
|
108
|
+
(p - 1) % pi == 0
|
|
109
|
+
and math.gcd(pi, int((p - 1) / pi)) == 1
|
|
110
|
+
and math.gcd(pi, q - 1) == 1
|
|
111
|
+
):
|
|
112
|
+
p_conditions.append(1)
|
|
113
|
+
else:
|
|
114
|
+
p_conditions.append(0)
|
|
115
|
+
p_satisfied = True if len(p_conditions) == sum(p_conditions) else False
|
|
116
|
+
if p_satisfied is False:
|
|
117
|
+
logger.debug("canceled because p_conditions are not satisfied")
|
|
118
|
+
continue
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
q_conditions = []
|
|
121
|
+
for i in range(int(k / 2), k):
|
|
122
|
+
pi = prime_set[i]
|
|
123
|
+
if (
|
|
124
|
+
(q - 1) % pi == 0
|
|
125
|
+
and math.gcd(pi, int((q - 1) / pi)) == 1
|
|
126
|
+
and math.gcd(pi, p - 1)
|
|
127
|
+
):
|
|
128
|
+
q_conditions.append(1)
|
|
129
|
+
else:
|
|
130
|
+
q_conditions.append(0)
|
|
131
|
+
|
|
132
|
+
q_satisfied = True if len(q_conditions) == sum(q_conditions) else False
|
|
133
|
+
if q_satisfied is False:
|
|
134
|
+
logger.debug("canceled because q_conditions are not satisfied")
|
|
135
|
+
continue
|
|
124
136
|
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
# Choose generator g
|
|
138
|
+
for _ in range(1000): # try max 1000 random g
|
|
139
|
+
g = random.randint(2, n - 1)
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# it must be co-prime to n
|
|
132
|
-
if math.gcd(g, n) != 1:
|
|
133
|
-
logger.debug("canceled becuase g is not co-prime with ne")
|
|
134
|
-
continue
|
|
135
|
-
# guarantee it is not pi-th power.
|
|
136
|
-
for pi in prime_set:
|
|
137
|
-
logger.debug("canceled because g is a pi-th power")
|
|
138
|
-
if pow(g, int(phi / pi), n) == 1:
|
|
141
|
+
# it must be co-prime to n
|
|
142
|
+
if math.gcd(g, n) != 1:
|
|
139
143
|
continue
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# check decryption is guaranteed similar to benaloh
|
|
146
|
-
# ps: this is not mentioned in the original paper
|
|
147
|
-
is_decryption_guaranteed = True
|
|
148
|
-
for pi in prime_set:
|
|
149
|
-
prime_factors = sympy.factorint(pi).keys()
|
|
150
|
-
for prime_factor in prime_factors:
|
|
151
|
-
if pow(g, int(phi / prime_factor), n) == 1:
|
|
152
|
-
is_decryption_guaranteed = False
|
|
153
|
-
if is_decryption_guaranteed is True:
|
|
145
|
+
# guarantee it is not pi-th power.
|
|
146
|
+
if any(pow(g, phi // pi, n) == 1 for pi in prime_set):
|
|
147
|
+
continue
|
|
154
148
|
break
|
|
149
|
+
else:
|
|
150
|
+
continue # if no g found, retry
|
|
155
151
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# sigma can optionally be secret in deterministic version
|
|
161
|
-
keys["public_key"]["sigma"] = sigma
|
|
152
|
+
# Original phi/4 order check omitted for practical large-key generation
|
|
153
|
+
if key_size < 40:
|
|
154
|
+
if pow(g, int(phi / 4), n) != 1:
|
|
155
|
+
continue
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
# Success
|
|
158
|
+
keys = {
|
|
159
|
+
"public_key": {"n": n, "g": g, "sigma": sigma},
|
|
160
|
+
"private_key": {
|
|
161
|
+
"a": a,
|
|
162
|
+
"b": b,
|
|
163
|
+
"p": p,
|
|
164
|
+
"q": q,
|
|
165
|
+
"phi": phi,
|
|
166
|
+
"prime_set": prime_set,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"Keys generated after {attempt+1} attempts, n bits: {n.bit_length()}"
|
|
171
|
+
)
|
|
172
|
+
return keys
|
|
167
173
|
|
|
168
|
-
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
f"Failed to generate Naccache-Stern keys after {max_tries} attempts."
|
|
176
|
+
"Try to rerun or decrease key size."
|
|
177
|
+
)
|
|
169
178
|
|
|
170
179
|
def generate_random_key(self) -> int:
|
|
171
180
|
"""
|
|
@@ -218,12 +227,12 @@ class NaccacheStern(Homomorphic):
|
|
|
218
227
|
|
|
219
228
|
remainders = []
|
|
220
229
|
for i, prime in enumerate(prime_set):
|
|
221
|
-
ci = pow(ciphertext, int(phi
|
|
230
|
+
ci = pow(ciphertext, int(phi // prime), n)
|
|
222
231
|
logger.debug(f"c_{i} = {ci}")
|
|
223
232
|
|
|
224
233
|
j = 0
|
|
225
234
|
while True:
|
|
226
|
-
if ci == pow(g, int((j * phi)
|
|
235
|
+
if ci == pow(g, int((j * phi) // prime), n):
|
|
227
236
|
logger.debug(f"m_{i} = {j}")
|
|
228
237
|
remainders.append(j)
|
|
229
238
|
break
|
|
@@ -257,7 +266,7 @@ class NaccacheStern(Homomorphic):
|
|
|
257
266
|
"""
|
|
258
267
|
return (ciphertext1 * ciphertext2) % self.ciphertext_modulo
|
|
259
268
|
|
|
260
|
-
def
|
|
269
|
+
def multiply_by_constant(self, ciphertext: int, constant: int) -> int:
|
|
261
270
|
"""
|
|
262
271
|
Multiply a ciphertext with a plain constant.
|
|
263
272
|
Result of this must be equal to E(m1 * constant) where E(m1) = ciphertext
|
|
@@ -127,7 +127,7 @@ class OkamotoUchiyama(Homomorphic):
|
|
|
127
127
|
n = self.keys["public_key"]["n"]
|
|
128
128
|
return (ciphertext1 * ciphertext2) % n
|
|
129
129
|
|
|
130
|
-
def
|
|
130
|
+
def multiply_by_constant(self, ciphertext: int, constant: int) -> int:
|
|
131
131
|
"""
|
|
132
132
|
Multiply a ciphertext with a plain constant.
|
|
133
133
|
Result of this must be equal to E(m1 * constant) where E(m1) = ciphertext
|
|
@@ -112,7 +112,7 @@ class Paillier(Homomorphic):
|
|
|
112
112
|
n = self.keys["public_key"]["n"]
|
|
113
113
|
return (ciphertext1 * ciphertext2) % (n * n)
|
|
114
114
|
|
|
115
|
-
def
|
|
115
|
+
def multiply_by_constant(self, ciphertext: int, constant: int) -> int:
|
|
116
116
|
"""
|
|
117
117
|
Multiply a ciphertext with a plain constant.
|
|
118
118
|
Result of this must be equal to E(m1 * m2) where E(m1) = ciphertext
|
|
@@ -95,12 +95,12 @@ class Ciphertext:
|
|
|
95
95
|
# Handle multiplication with another EncryptedObject
|
|
96
96
|
result = self.cs.multiply(ciphertext1=self.value, ciphertext2=other.value)
|
|
97
97
|
elif isinstance(other, int):
|
|
98
|
-
result = self.cs.
|
|
98
|
+
result = self.cs.multiply_by_constant(ciphertext=self.value, constant=other)
|
|
99
99
|
elif isinstance(other, float):
|
|
100
100
|
constant = phe_utils.normalize_input(
|
|
101
101
|
value=other, modulo=self.cs.plaintext_modulo
|
|
102
102
|
)
|
|
103
|
-
result = self.cs.
|
|
103
|
+
result = self.cs.multiply_by_constant(
|
|
104
104
|
ciphertext=self.value, constant=constant
|
|
105
105
|
)
|
|
106
106
|
else:
|
|
@@ -14,7 +14,9 @@ class Homomorphic(ABC):
|
|
|
14
14
|
ciphertext_modulo: int
|
|
15
15
|
|
|
16
16
|
@abstractmethod
|
|
17
|
-
def generate_keys(
|
|
17
|
+
def generate_keys(
|
|
18
|
+
self, key_size: int, s: Optional[int] = None, max_retries: Optional[int] = None
|
|
19
|
+
) -> dict:
|
|
18
20
|
pass
|
|
19
21
|
|
|
20
22
|
@abstractmethod
|
|
@@ -34,18 +36,28 @@ class Homomorphic(ABC):
|
|
|
34
36
|
def add(
|
|
35
37
|
self, ciphertext1: Union[int, tuple, list], ciphertext2: Union[int, tuple, list]
|
|
36
38
|
) -> Union[int, tuple, list]:
|
|
37
|
-
raise ValueError(
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"{self.get_algorithm_name()} is not homomorphic with respect to the addition"
|
|
41
|
+
)
|
|
38
42
|
|
|
39
43
|
def multiply(
|
|
40
44
|
self, ciphertext1: Union[int, tuple, list], ciphertext2: Union[int, tuple, list]
|
|
41
45
|
) -> Union[int, tuple]:
|
|
42
|
-
raise ValueError(
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"{self.get_algorithm_name()} is not homomorphic with respect to the multiplication"
|
|
48
|
+
)
|
|
43
49
|
|
|
44
50
|
def xor(self, ciphertext1: list, ciphertext2: list) -> list:
|
|
45
|
-
raise ValueError(
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"{self.get_algorithm_name()} is not homomorphic with respect to the exclusive or"
|
|
53
|
+
)
|
|
46
54
|
|
|
47
|
-
def
|
|
48
|
-
|
|
55
|
+
def multiply_by_constant(
|
|
56
|
+
self, ciphertext: Union[int, tuple, list], constant: int
|
|
57
|
+
) -> int:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"{self.get_algorithm_name()} is not supporting multiplying ciphertext by a known constant"
|
|
60
|
+
)
|
|
49
61
|
|
|
50
62
|
def reencrypt(self, ciphertext: Union[int, tuple, list]) -> Union[int, tuple, list]:
|
|
51
63
|
raise ValueError(f"{self.get_algorithm_name()} does not support re-encryption")
|
|
@@ -53,5 +65,7 @@ class Homomorphic(ABC):
|
|
|
53
65
|
def get_algorithm_name(self) -> str:
|
|
54
66
|
class_name = self.__class__.__name__
|
|
55
67
|
algorithm_name = getattr(Algorithm, class_name, None)
|
|
56
|
-
assert isinstance(
|
|
68
|
+
assert isinstance(
|
|
69
|
+
algorithm_name, str
|
|
70
|
+
), f"Algorithm name for {class_name} is not defined"
|
|
57
71
|
return algorithm_name
|
|
@@ -268,10 +268,10 @@ class EncryptedTensor:
|
|
|
268
268
|
|
|
269
269
|
fractions = []
|
|
270
270
|
for alpha_tensor in self.fractions:
|
|
271
|
-
dividend = self.cs.
|
|
271
|
+
dividend = self.cs.multiply_by_constant(
|
|
272
272
|
ciphertext=alpha_tensor.dividend, constant=other
|
|
273
273
|
)
|
|
274
|
-
abs_dividend = self.cs.
|
|
274
|
+
abs_dividend = self.cs.multiply_by_constant(
|
|
275
275
|
ciphertext=alpha_tensor.abs_dividend, constant=other
|
|
276
276
|
)
|
|
277
277
|
# notice that divisor is alpha tensor's divisor instead of addition
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lightphe
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.16
|
|
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
|
|
@@ -28,7 +28,7 @@ License-File: LICENSE
|
|
|
28
28
|
[](https://www.youtube.com/@sefiks?sub_confirmation=1)
|
|
29
29
|
[](https://twitter.com/intent/user?screen_name=serengil)
|
|
30
30
|
|
|
31
|
-
[](https://www.patreon.com/serengil?repo=lightphe)
|
|
32
32
|
[](https://github.com/sponsors/serengil)
|
|
33
33
|
[](https://buymeacoffee.com/serengil)
|
|
34
34
|
|
|
@@ -262,7 +262,7 @@ One of the crucial factors that define the security level of an elliptic curve c
|
|
|
262
262
|
|
|
263
263
|
Each curve in LightPHE has a specific order, which is carefully chosen to balance performance and security. By selecting an elliptic curve with a larger order, you increase the security of your cryptographic system, but this may come with a trade-off in computational efficiency. Therefore, choosing the appropriate curve order is a crucial decision based on your application’s security and performance requirements.
|
|
264
264
|
|
|
265
|
-
See [`curves`](https://github.com/serengil/LightECC) page for a list of all supported forms, curves and their details.
|
|
265
|
+
See [`curves`](https://github.com/serengil/LightECC#supported-curves) page for a list of all supported forms, curves and their details.
|
|
266
266
|
|
|
267
267
|
### Vector Embeddings and Tensors
|
|
268
268
|
|
|
@@ -379,6 +379,12 @@ calculated_similarity = cs.decrypt(encrypted_cosine_similarity)[0]
|
|
|
379
379
|
assert abs(calculated_similarity - expected_similarity) < 1e-2
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
+
## Security Concerns and Considerations
|
|
383
|
+
|
|
384
|
+
While LightPHE enables encryption using public keys and supports homomorphic operations on encrypted data, it's important to understand a subtle but critical security implication of this approach. Since the it is homomorphic, the cloud (or any third-party processor) can perform computations without knowing the plaintext. However, a malicious actor can still encrypt arbitrary values and craft valid ciphertexts because encryption depends on your public key and it is publicly known. For example, even if a salary is encrypted, an attacker could encrypt 500 USD using your public key and update the encrypted salary with this forged ciphertext — without needing to know the original value.
|
|
385
|
+
|
|
386
|
+
To mitigate this, we recommend combining LightPHE with digital signature schemes. By keeping a signed audit trail of update operations in a separate log table, you can detect and reject unauthorized changes, and even restore the original data in case of tampering. We suggest checking out [`LightDSA`](https://github.com/serengil/LightDSA), which provides a lightweight cryptographic interface similar to LightPHE and supports popular digital signature algorithms like RSA, DSA, ECDSA, and EdDSA.
|
|
387
|
+
|
|
382
388
|
# Contributing
|
|
383
389
|
|
|
384
390
|
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.
|
|
@@ -392,11 +398,11 @@ There are many ways to support a project - starring⭐️ the GitHub repo is jus
|
|
|
392
398
|
You can also support this work on [Patreon](https://www.patreon.com/serengil?repo=lightphe), [GitHub Sponsors](https://github.com/sponsors/serengil) or [Buy Me a Coffee](https://buymeacoffee.com/serengil).
|
|
393
399
|
|
|
394
400
|
<a href="https://www.patreon.com/serengil?repo=lightphe">
|
|
395
|
-
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/patreon.png" width="30%"
|
|
401
|
+
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/patreon.png" width="30%">
|
|
396
402
|
</a>
|
|
397
403
|
|
|
398
404
|
<a href="https://buymeacoffee.com/serengil">
|
|
399
|
-
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/bmc-button.png" width="25%"
|
|
405
|
+
<img src="https://raw.githubusercontent.com/serengil/LightPHE/master/icons/bmc-button.png" width="25%">
|
|
400
406
|
</a>
|
|
401
407
|
|
|
402
408
|
Also, your company's logo will be shown on README on GitHub if you become sponsor in gold, silver or bronze tiers.
|
|
@@ -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.16",
|
|
12
12
|
author="Sefik Ilkin Serengil",
|
|
13
13
|
author_email="serengil@gmail.com",
|
|
14
14
|
description="A Lightweight Partially Homomorphic Encryption Library for Python",
|
|
@@ -16,7 +16,7 @@ def test_benaloh():
|
|
|
16
16
|
|
|
17
17
|
# supported homomorphic operations
|
|
18
18
|
assert bn.decrypt(bn.add(c1, c2)) == (m1 + m2) % bn.plaintext_modulo
|
|
19
|
-
assert bn.decrypt(bn.
|
|
19
|
+
assert bn.decrypt(bn.multiply_by_constant(c1, m2)) == (m1 * m2) % bn.plaintext_modulo
|
|
20
20
|
|
|
21
21
|
# unsupported homomorphic operations
|
|
22
22
|
with pytest.raises(ValueError):
|
|
@@ -16,7 +16,7 @@ def test_damgardjurik():
|
|
|
16
16
|
|
|
17
17
|
# homomorphic operations
|
|
18
18
|
assert dg.decrypt(dg.add(c1, c2)) == (m1 + m2) % dg.plaintext_modulo
|
|
19
|
-
assert dg.decrypt(dg.
|
|
19
|
+
assert dg.decrypt(dg.multiply_by_constant(c1, m2)) == (m1 * m2) % dg.plaintext_modulo
|
|
20
20
|
|
|
21
21
|
# unsupported homomorphic operations
|
|
22
22
|
with pytest.raises(ValueError):
|
|
@@ -26,7 +26,7 @@ def test_elgamal():
|
|
|
26
26
|
eg.xor(c1, c2)
|
|
27
27
|
|
|
28
28
|
with pytest.raises(ValueError):
|
|
29
|
-
eg.
|
|
29
|
+
eg.multiply_by_constant(c1, m2)
|
|
30
30
|
|
|
31
31
|
# re-encryption
|
|
32
32
|
c1_prime = eg.reencrypt(c1)
|
|
@@ -54,7 +54,7 @@ def test_exponential_elgamal():
|
|
|
54
54
|
# homomorphic operations
|
|
55
55
|
assert additive_eg.decrypt(c3) == (m1 + m2) % additive_eg.plaintext_modulo
|
|
56
56
|
assert (
|
|
57
|
-
additive_eg.decrypt(additive_eg.
|
|
57
|
+
additive_eg.decrypt(additive_eg.multiply_by_constant(c1, m2))
|
|
58
58
|
== (m1 * m2) % additive_eg.plaintext_modulo
|
|
59
59
|
)
|
|
60
60
|
|
|
@@ -8,6 +8,8 @@ logger = Logger(module="tests/test_goldwasser.py")
|
|
|
8
8
|
def test_goldwasser():
|
|
9
9
|
cs = GoldwasserMicali()
|
|
10
10
|
|
|
11
|
+
security_level = cs.keys["public_key"]["n"].bit_length()
|
|
12
|
+
|
|
11
13
|
m1 = 17
|
|
12
14
|
m2 = 27
|
|
13
15
|
|
|
@@ -29,9 +31,11 @@ def test_goldwasser():
|
|
|
29
31
|
cs.add(c1, c2)
|
|
30
32
|
|
|
31
33
|
with pytest.raises(ValueError):
|
|
32
|
-
cs.
|
|
34
|
+
cs.multiply_by_constant(c1, c2)
|
|
33
35
|
|
|
34
|
-
logger.info(
|
|
36
|
+
logger.info(
|
|
37
|
+
f"✅ Goldwasser-Micali test succeeded ({security_level} bit security level)"
|
|
38
|
+
)
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def test_api():
|
|
@@ -39,6 +43,8 @@ def test_api():
|
|
|
39
43
|
|
|
40
44
|
cs = LightPHE(algorithm_name="Goldwasser-Micali")
|
|
41
45
|
|
|
46
|
+
security_level = cs.cs.keys["public_key"]["n"].bit_length()
|
|
47
|
+
|
|
42
48
|
# try different bit size messages
|
|
43
49
|
ms = [(17, 21), (117, 23), (23, 117), (1117, 23), (12, 1118)]
|
|
44
50
|
|
|
@@ -63,5 +69,6 @@ def test_api():
|
|
|
63
69
|
_ = 5 * c1
|
|
64
70
|
|
|
65
71
|
logger.info(
|
|
66
|
-
f"✅ Goldwasser-Micali api test succeeded for {m1.bit_length()}&{m2.bit_length()} bit plaintexts"
|
|
72
|
+
f"✅ Goldwasser-Micali api test succeeded for {m1.bit_length()}&{m2.bit_length()} bit plaintexts "
|
|
73
|
+
f"({security_level} bit security level)"
|
|
67
74
|
)
|
|
@@ -24,7 +24,12 @@ def test_key_restoration():
|
|
|
24
24
|
for algorithm_name in algorithms:
|
|
25
25
|
private_key_file = f"/tmp/{algorithm_name}_secret.json"
|
|
26
26
|
public_key_file = f"/tmp/{algorithm_name}_public.json"
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
# unfortunately Naccache-Stern key generation isn't guaranteed to be completed
|
|
29
|
+
if algorithm_name == "Naccache-Stern":
|
|
30
|
+
onprem_cs = LightPHE(algorithm_name=algorithm_name, key_size=37)
|
|
31
|
+
else:
|
|
32
|
+
onprem_cs = LightPHE(algorithm_name=algorithm_name)
|
|
28
33
|
onprem_cs.export_keys(private_key_file)
|
|
29
34
|
onprem_cs.export_keys(public_key_file, public=True)
|
|
30
35
|
del onprem_cs
|
|
@@ -57,4 +62,4 @@ def test_key_restoration():
|
|
|
57
62
|
assert onprem_cs.decrypt(c3) == m1 + m2
|
|
58
63
|
assert onprem_cs.decrypt(c4) == k * m1
|
|
59
64
|
|
|
60
|
-
logger.info(f"✅ Key restoration
|
|
65
|
+
logger.info(f"✅ Key restoration test succeeded for {algorithm_name}")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import pytest
|
|
3
|
+
from lightphe.cryptosystems.NaccacheStern import NaccacheStern
|
|
4
|
+
from lightphe.commons.logger import Logger
|
|
5
|
+
|
|
6
|
+
logger = Logger(module="tests/test_naccache.py")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_naccache_on_plaintexts():
|
|
10
|
+
cs = NaccacheStern(key_size=37, deterministic=False)
|
|
11
|
+
|
|
12
|
+
m1 = 17
|
|
13
|
+
m2 = 21
|
|
14
|
+
|
|
15
|
+
c1 = cs.encrypt(m1)
|
|
16
|
+
c2 = cs.encrypt(m2)
|
|
17
|
+
|
|
18
|
+
assert cs.decrypt(c1) == m1 % cs.plaintext_modulo
|
|
19
|
+
assert cs.decrypt(c2) == m2 % cs.plaintext_modulo
|
|
20
|
+
|
|
21
|
+
# homomorphic operations
|
|
22
|
+
assert cs.decrypt(cs.add(c1, c2)) == (m1 + m2) % cs.plaintext_modulo
|
|
23
|
+
assert (
|
|
24
|
+
cs.decrypt(cs.multiply_by_constant(c2, m1)) == (m1 * m2) % cs.plaintext_modulo
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# unsupported operations
|
|
28
|
+
with pytest.raises(ValueError):
|
|
29
|
+
cs.multiply(c1, c2)
|
|
30
|
+
|
|
31
|
+
with pytest.raises(ValueError):
|
|
32
|
+
cs.xor(c1, c2)
|
|
33
|
+
|
|
34
|
+
# re-encryption
|
|
35
|
+
c1_prime = cs.reencrypt(c1)
|
|
36
|
+
assert c1_prime != c1
|
|
37
|
+
assert cs.decrypt(c1_prime) == m1 % cs.plaintext_modulo
|
|
38
|
+
logger.info("✅ Probabilistic Naccache-Stern test succeeded")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_deterministic_version():
|
|
42
|
+
cs = NaccacheStern(deterministic=True, key_size=37)
|
|
43
|
+
|
|
44
|
+
m1 = 17
|
|
45
|
+
m2 = 21
|
|
46
|
+
|
|
47
|
+
c1 = cs.encrypt(m1)
|
|
48
|
+
c2 = cs.encrypt(m2)
|
|
49
|
+
|
|
50
|
+
assert cs.decrypt(c1) == m1 % cs.plaintext_modulo
|
|
51
|
+
assert cs.decrypt(c2) == m2 % cs.plaintext_modulo
|
|
52
|
+
|
|
53
|
+
# homomorphic operations
|
|
54
|
+
assert cs.decrypt(cs.add(c1, c2)) == (m1 + m2) % cs.plaintext_modulo
|
|
55
|
+
assert (
|
|
56
|
+
cs.decrypt(cs.multiply_by_constant(c2, m1)) == (m1 * m2) % cs.plaintext_modulo
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# unsupported operations
|
|
60
|
+
with pytest.raises(ValueError):
|
|
61
|
+
cs.multiply(c1, c2)
|
|
62
|
+
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
cs.xor(c1, c2)
|
|
65
|
+
|
|
66
|
+
with pytest.raises(ValueError):
|
|
67
|
+
cs.reencrypt(c1)
|
|
68
|
+
|
|
69
|
+
logger.info("✅ Deterministic Naccache-Stern test succeeded")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_predefined_keys():
|
|
73
|
+
"""
|
|
74
|
+
Key generation of Naccache-Stern is probabilistic and may take time.
|
|
75
|
+
and may not succeed sometimes. Test encrypt, decrypt and homomorphic
|
|
76
|
+
operations performances with predefined keys.
|
|
77
|
+
"""
|
|
78
|
+
keys = {
|
|
79
|
+
"public_key": {
|
|
80
|
+
"n": 75275183223356977395233015128636801841182684353869611205877871399639096259927846711964449336873721703958210179653876541687516247046310691985977881277598653839478670263258301833023097636036737220721033604997817044724812266620826468829212603338614968953684914579782761554925497589591101127424868060977977362624693729,
|
|
81
|
+
"g": 2322858651861384064537514431822509724460178869240342120700966005387085547272373554253264326744794826709642720143603083090045603237528622393527254537059946505389885957837759097286992580665451178640443489482838164247603492906332153298019557819758033383394570549848334276102748452532710144646257390863266986629778942,
|
|
82
|
+
"sigma": 255255,
|
|
83
|
+
},
|
|
84
|
+
"private_key": {
|
|
85
|
+
"a": 11597041531885171702428009785553891872754255798746286340205448513302430749967682685453809973100036260110796582178813477436613205882229456042479325870287379,
|
|
86
|
+
"b": 6357265649357031680615244845679689252112451833267362870520093680285536565908774220658104716644696735431871010152670595233003071273441498122136618828384989,
|
|
87
|
+
"p": 2435378721695886057509882054966317293278393717736720131443144187793510457493213363945300094351007614623267282257550830261688773235268185768920658432760349591,
|
|
88
|
+
"q": 30909025587173888031151320439694649143770740813345918276468695473548278783448460260839705132326515527669756851362284434022860932531472563869828240743607816519,
|
|
89
|
+
"phi": 75275183223356977395233015128636801841182684353869611205877871399639096259927846711964449336873721703958210179653876541687516247046310691985977881277598653806134265954388527744361895141375770783671899073915178636812972605279037227887538978553609742276161772286758627935090233305041395360684118422229078186256527620,
|
|
90
|
+
"prime_set": [3, 5, 7, 11, 13, 17],
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
from lightphe import LightPHE
|
|
95
|
+
|
|
96
|
+
cs = LightPHE(algorithm_name="Naccache-Stern", keys=keys)
|
|
97
|
+
|
|
98
|
+
security_level = cs.cs.keys["public_key"]["n"].bit_length()
|
|
99
|
+
|
|
100
|
+
logger.info(
|
|
101
|
+
f"Naccache-Stern cs built with {security_level} bits key. Recommended >= 768."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
m1 = 17
|
|
105
|
+
m2 = 21
|
|
106
|
+
|
|
107
|
+
tic = time.time()
|
|
108
|
+
c1 = cs.encrypt(plaintext=m1)
|
|
109
|
+
c2 = cs.encrypt(plaintext=m2)
|
|
110
|
+
toc = time.time()
|
|
111
|
+
logger.info(f"Naccache-Stern encryption time: {(toc - tic)/2:.4f} seconds")
|
|
112
|
+
|
|
113
|
+
# homomorphic addition
|
|
114
|
+
tic = time.time()
|
|
115
|
+
assert cs.decrypt(c1 + c2) == m1 + m2
|
|
116
|
+
toc = time.time()
|
|
117
|
+
logger.info(f"Naccache-Stern homomorphic addition time: {(toc - tic):.4f} seconds")
|
|
118
|
+
|
|
119
|
+
# homomorphic scalar multiplication
|
|
120
|
+
tic = time.time()
|
|
121
|
+
c1_times_m2 = c1 * m2
|
|
122
|
+
c2_times_m1 = c2 * m1
|
|
123
|
+
m2_times_c1 = m2 * c1
|
|
124
|
+
m1_times_c2 = m1 * c2
|
|
125
|
+
toc = time.time()
|
|
126
|
+
logger.info(
|
|
127
|
+
f"Naccache-Stern homomorphic scalar multiplication time: {(toc - tic)/4:.4f} seconds"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert cs.decrypt(c1_times_m2) == m1 * m2
|
|
131
|
+
assert cs.decrypt(c2_times_m1) == m1 * m2
|
|
132
|
+
assert cs.decrypt(m2_times_c1) == m1 * m2
|
|
133
|
+
assert cs.decrypt(m1_times_c2) == m1 * m2
|
|
134
|
+
|
|
135
|
+
# unsupported homomorphic operations
|
|
136
|
+
with pytest.raises(ValueError):
|
|
137
|
+
_ = c1 * c2
|
|
138
|
+
|
|
139
|
+
with pytest.raises(ValueError):
|
|
140
|
+
_ = c1 ^ c2
|
|
141
|
+
|
|
142
|
+
logger.info("✅ Naccache-Stern api test with pre-defined keys succeeded")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_api():
|
|
146
|
+
from lightphe import LightPHE
|
|
147
|
+
|
|
148
|
+
cs = LightPHE(algorithm_name="Naccache-Stern", key_size=37)
|
|
149
|
+
|
|
150
|
+
m1 = 17
|
|
151
|
+
m2 = 21
|
|
152
|
+
|
|
153
|
+
c1 = cs.encrypt(plaintext=m1)
|
|
154
|
+
c2 = cs.encrypt(plaintext=m2)
|
|
155
|
+
|
|
156
|
+
# homomorphic addition
|
|
157
|
+
assert cs.decrypt(c1 + c2) == m1 + m2
|
|
158
|
+
|
|
159
|
+
# homomorphic scalar multiplication
|
|
160
|
+
assert cs.decrypt(c1 * m2) == m1 * m2
|
|
161
|
+
assert cs.decrypt(c2 * m1) == m1 * m2
|
|
162
|
+
assert cs.decrypt(m2 * c1) == m1 * m2
|
|
163
|
+
assert cs.decrypt(m1 * c2) == m1 * m2
|
|
164
|
+
|
|
165
|
+
# unsupported homomorphic operations
|
|
166
|
+
with pytest.raises(ValueError):
|
|
167
|
+
_ = c1 * c2
|
|
168
|
+
|
|
169
|
+
with pytest.raises(ValueError):
|
|
170
|
+
_ = c1 ^ c2
|
|
171
|
+
|
|
172
|
+
logger.info("✅ Naccache-Stern api test succeeded")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.skip(reason="We cannot generate Naccache-Stern keys always")
|
|
176
|
+
def test_api_with_real_keys():
|
|
177
|
+
from lightphe import LightPHE
|
|
178
|
+
|
|
179
|
+
cs = LightPHE(algorithm_name="Naccache-Stern")
|
|
180
|
+
|
|
181
|
+
m1 = 17
|
|
182
|
+
m2 = 21
|
|
183
|
+
|
|
184
|
+
c1 = cs.encrypt(plaintext=m1)
|
|
185
|
+
c2 = cs.encrypt(plaintext=m2)
|
|
186
|
+
|
|
187
|
+
# homomorphic addition
|
|
188
|
+
assert cs.decrypt(c1 + c2) == m1 + m2
|
|
189
|
+
|
|
190
|
+
# homomorphic scalar multiplication
|
|
191
|
+
assert cs.decrypt(c1 * m2) == m1 * m2
|
|
192
|
+
assert cs.decrypt(c2 * m1) == m1 * m2
|
|
193
|
+
assert cs.decrypt(m2 * c1) == m1 * m2
|
|
194
|
+
assert cs.decrypt(m1 * c2) == m1 * m2
|
|
195
|
+
|
|
196
|
+
# unsupported homomorphic operations
|
|
197
|
+
with pytest.raises(ValueError):
|
|
198
|
+
_ = c1 * c2
|
|
199
|
+
|
|
200
|
+
with pytest.raises(ValueError):
|
|
201
|
+
_ = c1 ^ c2
|
|
202
|
+
|
|
203
|
+
logger.info("✅ Naccache-Stern api test succeeded")
|
|
@@ -15,7 +15,7 @@ def test_okamoto():
|
|
|
15
15
|
|
|
16
16
|
# homomorphic operations
|
|
17
17
|
assert cs.decrypt(cs.add(c1, c2)) == (m1 + m2) % cs.plaintext_modulo
|
|
18
|
-
assert cs.decrypt(cs.
|
|
18
|
+
assert cs.decrypt(cs.multiply_by_constant(c1, m2)) == (m1 * m2) % cs.plaintext_modulo
|
|
19
19
|
|
|
20
20
|
# unsupported operations
|
|
21
21
|
with pytest.raises(ValueError, match="Okamoto-Uchiyama is not homomorphic with respect to the multiplication"):
|
|
@@ -16,7 +16,7 @@ def test_paillier():
|
|
|
16
16
|
|
|
17
17
|
# homomorphic operations
|
|
18
18
|
assert pai.decrypt(pai.add(c1, c2)) == (m1 + m2) % pai.plaintext_modulo
|
|
19
|
-
assert pai.decrypt(pai.
|
|
19
|
+
assert pai.decrypt(pai.multiply_by_constant(c1, m2)) == (m1 * m2) % pai.plaintext_modulo
|
|
20
20
|
|
|
21
21
|
# unsupported homomorphic operations
|
|
22
22
|
with pytest.raises(ValueError):
|
|
@@ -26,7 +26,7 @@ def test_salary():
|
|
|
26
26
|
ratio = 105 * pow(100, -1, cs.plaintext_modulo)
|
|
27
27
|
|
|
28
28
|
# final salary should be 11000 x 1.05 = 11500
|
|
29
|
-
final_salary_encrytped = cs.
|
|
29
|
+
final_salary_encrytped = cs.multiply_by_constant(new_salary_encrypted, ratio)
|
|
30
30
|
assert cs.decrypt(final_salary_encrytped) == 11550
|
|
31
31
|
|
|
32
32
|
logger.info("✅ Salary test succeeded")
|
lightphe-0.0.15/requirements.txt
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from lightphe.cryptosystems.NaccacheStern import NaccacheStern
|
|
3
|
-
from lightphe.commons.logger import Logger
|
|
4
|
-
|
|
5
|
-
logger = Logger(module="tests/test_naccache.py")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_naccache_on_plaintexts():
|
|
9
|
-
cs = NaccacheStern(key_size=37, deterministic=False)
|
|
10
|
-
|
|
11
|
-
m1 = 17
|
|
12
|
-
m2 = 21
|
|
13
|
-
|
|
14
|
-
c1 = cs.encrypt(m1)
|
|
15
|
-
c2 = cs.encrypt(m2)
|
|
16
|
-
|
|
17
|
-
assert cs.decrypt(c1) == m1 % cs.plaintext_modulo
|
|
18
|
-
assert cs.decrypt(c2) == m2 % cs.plaintext_modulo
|
|
19
|
-
|
|
20
|
-
# homomorphic operations
|
|
21
|
-
assert cs.decrypt(cs.add(c1, c2)) == (m1 + m2) % cs.plaintext_modulo
|
|
22
|
-
assert cs.decrypt(cs.multiply_by_contant(c2, m1)) == (m1 * m2) % cs.plaintext_modulo
|
|
23
|
-
|
|
24
|
-
# unsupported operations
|
|
25
|
-
with pytest.raises(ValueError):
|
|
26
|
-
cs.multiply(c1, c2)
|
|
27
|
-
|
|
28
|
-
with pytest.raises(ValueError):
|
|
29
|
-
cs.xor(c1, c2)
|
|
30
|
-
|
|
31
|
-
# re-encryption
|
|
32
|
-
c1_prime = cs.reencrypt(c1)
|
|
33
|
-
assert c1_prime != c1
|
|
34
|
-
assert cs.decrypt(c1_prime) == m1 % cs.plaintext_modulo
|
|
35
|
-
logger.info("✅ Probabilistic Naccache-Stern test succeeded")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_deterministic_version():
|
|
39
|
-
cs = NaccacheStern(deterministic=True)
|
|
40
|
-
|
|
41
|
-
m1 = 17
|
|
42
|
-
m2 = 21
|
|
43
|
-
|
|
44
|
-
c1 = cs.encrypt(m1)
|
|
45
|
-
c2 = cs.encrypt(m2)
|
|
46
|
-
|
|
47
|
-
assert cs.decrypt(c1) == m1 % cs.plaintext_modulo
|
|
48
|
-
assert cs.decrypt(c2) == m2 % cs.plaintext_modulo
|
|
49
|
-
|
|
50
|
-
# homomorphic operations
|
|
51
|
-
assert cs.decrypt(cs.add(c1, c2)) == (m1 + m2) % cs.plaintext_modulo
|
|
52
|
-
assert cs.decrypt(cs.multiply_by_contant(c2, m1)) == (m1 * m2) % cs.plaintext_modulo
|
|
53
|
-
|
|
54
|
-
# unsupported operations
|
|
55
|
-
with pytest.raises(ValueError):
|
|
56
|
-
cs.multiply(c1, c2)
|
|
57
|
-
|
|
58
|
-
with pytest.raises(ValueError):
|
|
59
|
-
cs.xor(c1, c2)
|
|
60
|
-
|
|
61
|
-
with pytest.raises(ValueError):
|
|
62
|
-
cs.reencrypt(c1)
|
|
63
|
-
|
|
64
|
-
logger.info("✅ Deterministic Naccache-Stern test succeeded")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_api():
|
|
68
|
-
from lightphe import LightPHE
|
|
69
|
-
|
|
70
|
-
cs = LightPHE(algorithm_name="Naccache-Stern")
|
|
71
|
-
|
|
72
|
-
m1 = 17
|
|
73
|
-
m2 = 21
|
|
74
|
-
|
|
75
|
-
c1 = cs.encrypt(plaintext=m1)
|
|
76
|
-
c2 = cs.encrypt(plaintext=m2)
|
|
77
|
-
|
|
78
|
-
# homomorphic addition
|
|
79
|
-
assert cs.decrypt(c1 + c2) == m1 + m2
|
|
80
|
-
|
|
81
|
-
# homomorphic scalar multiplication
|
|
82
|
-
assert cs.decrypt(c1 * m2) == m1 * m2
|
|
83
|
-
assert cs.decrypt(c2 * m1) == m1 * m2
|
|
84
|
-
assert cs.decrypt(m2 * c1) == m1 * m2
|
|
85
|
-
assert cs.decrypt(m1 * c2) == m1 * m2
|
|
86
|
-
|
|
87
|
-
# unsupported homomorphic operations
|
|
88
|
-
with pytest.raises(ValueError):
|
|
89
|
-
_ = c1 * c2
|
|
90
|
-
|
|
91
|
-
with pytest.raises(ValueError):
|
|
92
|
-
_ = c1 ^ c2
|
|
93
|
-
|
|
94
|
-
logger.info("✅ Naccache-Stern api test succeeded")
|
|
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
|