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.
Files changed (49) hide show
  1. {lightphe-0.0.15 → lightphe-0.0.16}/PKG-INFO +11 -5
  2. {lightphe-0.0.15 → lightphe-0.0.16}/README.md +10 -4
  3. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/__init__.py +1 -1
  4. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/Benaloh.py +1 -1
  5. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/DamgardJurik.py +1 -1
  6. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/ElGamal.py +1 -1
  7. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/EllipticCurveElGamal.py +1 -1
  8. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/GoldwasserMicali.py +33 -30
  9. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/NaccacheStern.py +103 -94
  10. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/OkamotoUchiyama.py +1 -1
  11. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/Paillier.py +1 -1
  12. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Ciphertext.py +2 -2
  13. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Homomorphic.py +21 -7
  14. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Tensor.py +2 -2
  15. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/PKG-INFO +11 -5
  16. lightphe-0.0.16/lightphe.egg-info/requires.txt +4 -0
  17. lightphe-0.0.16/requirements.txt +4 -0
  18. {lightphe-0.0.15 → lightphe-0.0.16}/setup.py +1 -1
  19. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_benaloh.py +1 -1
  20. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_damgard.py +1 -1
  21. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_elgamal.py +2 -2
  22. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_ellipticcurveelgamal.py +1 -1
  23. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_goldwasser.py +10 -3
  24. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_keys.py +7 -2
  25. lightphe-0.0.16/tests/test_naccache.py +203 -0
  26. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_okamoto.py +1 -1
  27. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_paillier.py +1 -1
  28. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_rsa.py +1 -1
  29. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_salary.py +1 -1
  30. lightphe-0.0.15/lightphe.egg-info/requires.txt +0 -4
  31. lightphe-0.0.15/requirements.txt +0 -4
  32. lightphe-0.0.15/tests/test_naccache.py +0 -94
  33. {lightphe-0.0.15 → lightphe-0.0.16}/LICENSE +0 -0
  34. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/commons/__init__.py +0 -0
  35. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/commons/logger.py +0 -0
  36. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/commons/phe_utils.py +0 -0
  37. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/RSA.py +0 -0
  38. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/cryptosystems/__init__.py +0 -0
  39. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/Algorithm.py +0 -0
  40. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe/models/__init__.py +0 -0
  41. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/SOURCES.txt +0 -0
  42. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/dependency_links.txt +0 -0
  43. {lightphe-0.0.15 → lightphe-0.0.16}/lightphe.egg-info/top_level.txt +0 -0
  44. {lightphe-0.0.15 → lightphe-0.0.16}/setup.cfg +0 -0
  45. {lightphe-0.0.15 → lightphe-0.0.16}/tests/__init__.py +0 -0
  46. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_ciphertexts.py +0 -0
  47. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_cloud.py +0 -0
  48. {lightphe-0.0.15 → lightphe-0.0.16}/tests/test_exporting_keys.py +0 -0
  49. {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.15
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
  [![YouTube](https://img.shields.io/:youtube-@sefiks-red.svg?style=flat&logo=youtube)](https://www.youtube.com/@sefiks?sub_confirmation=1)
29
29
  [![Twitter](https://img.shields.io/:follow-@serengil-blue.svg?style=flat&logo=x)](https://twitter.com/intent/user?screen_name=serengil)
30
30
 
31
- [![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)
31
+ [![Patreon](https://img.shields.io/:become-patron-f96854.svg?style=flat&logo=patreon)](https://www.patreon.com/serengil?repo=lightphe)
32
32
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/serengil?logo=GitHub&color=lightgray)](https://github.com/sponsors/serengil)
33
33
  [![Buy Me a Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](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%" height="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%" height="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
  [![YouTube](https://img.shields.io/:youtube-@sefiks-red.svg?style=flat&logo=youtube)](https://www.youtube.com/@sefiks?sub_confirmation=1)
13
13
  [![Twitter](https://img.shields.io/:follow-@serengil-blue.svg?style=flat&logo=x)](https://twitter.com/intent/user?screen_name=serengil)
14
14
 
15
- [![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)
15
+ [![Patreon](https://img.shields.io/:become-patron-f96854.svg?style=flat&logo=patreon)](https://www.patreon.com/serengil?repo=lightphe)
16
16
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/serengil?logo=GitHub&color=lightgray)](https://github.com/sponsors/serengil)
17
17
  [![Buy Me a Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](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%" height="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%" height="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.
@@ -38,7 +38,7 @@ from lightphe.cryptosystems.EllipticCurveElGamal import EllipticCurveElGamal
38
38
 
39
39
  logger = Logger(module="lightphe/__init__.py")
40
40
 
41
- VERSION = "0.0.15"
41
+ VERSION = "0.0.16"
42
42
 
43
43
 
44
44
  class LightPHE:
@@ -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 multiply_by_contant(self, ciphertext: int, constant: int) -> int:
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 multiply_by_contant(self, ciphertext: int, constant: int) -> int:
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 multiply_by_contant(self, ciphertext: tuple, constant: int) -> tuple:
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 multiply_by_contant(self, ciphertext: tuple, constant: int) -> tuple:
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
- # key size should be several hundred bits or more
33
- self.keys = keys or self.generate_keys(key_size or 100)
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
- keys = {}
47
- keys["private_key"] = {}
48
- keys["public_key"] = {}
49
-
50
- # picking a prime p
51
- p = sympy.randprime(200, 2 ** int(key_size / 2) - 1)
52
- assert isinstance(p, int)
53
-
54
- # picking a prime q
55
- q = sympy.randprime(200, 2 ** int(key_size / 2) - 1)
56
- assert isinstance(q, int)
57
-
58
- n = p * q
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
- # find non-residue x
61
- while True:
62
- x = random.randint(1, n - 1)
63
- if (
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
- keys["public_key"]["n"] = n
71
- keys["public_key"]["x"] = x
73
+ logger.debug(
74
+ f"Goldwasser-Micali keys generated after {attempt+1} attempts, n bits: {n.bit_length()}"
75
+ )
72
76
 
73
- keys["private_key"]["p"] = p
74
- keys["private_key"]["q"] = q
77
+ return keys
75
78
 
76
- return keys
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__(self, keys: Optional[dict] = None, key_size: Optional[int] = None, deterministic: bool = False):
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 37)
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): having private_key and public_key keys
51
+ keys (dict): containing 'private_key' and 'public_key'
45
52
  """
46
- keys = {}
47
- keys["private_key"] = {}
48
- keys["public_key"] = {}
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 < len(prime_set) / 2:
64
- u = u * prime
66
+ if i < k // 2:
67
+ u *= prime
65
68
  else:
66
- v = v * prime
69
+ v *= prime
67
70
 
68
71
  # product of all primes
69
72
  sigma = u * v
70
73
 
71
- # pick large prime numbers
72
- while True:
73
- a = sympy.randprime(200, 2 ** int(key_size / 2) - 1)
74
- b = sympy.randprime(100, a)
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 = (2 * a * u) + 1
78
- q = (2 * b * v) + 1
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
- for i in range(0, int(k / 2)):
94
- pi = prime_set[i]
95
- if (
96
- (p - 1) % pi == 0
97
- and math.gcd(pi, int((p - 1) / pi)) == 1
98
- and math.gcd(pi, q - 1) == 1
99
- ):
100
- p_conditions.append(1)
101
- else:
102
- p_conditions.append(0)
103
- p_satisfied = True if len(p_conditions) == sum(p_conditions) else False
104
- if p_satisfied is False:
105
- logger.debug("canceled because p_conditions are not satisfied")
106
- continue
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
- q_conditions = []
109
- for i in range(int(k / 2), k):
110
- pi = prime_set[i]
111
- if (
112
- (q - 1) % pi == 0
113
- and math.gcd(pi, int((q - 1) / pi)) == 1
114
- and math.gcd(pi, p - 1)
115
- ):
116
- q_conditions.append(1)
117
- else:
118
- q_conditions.append(0)
119
-
120
- q_satisfied = True if len(q_conditions) == sum(q_conditions) else False
121
- if q_satisfied is False:
122
- logger.debug("canceled because q_conditions are not satisfied")
123
- continue
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
- # p and q must be primes
126
- if not (sympy.isprime(p) and sympy.isprime(q)):
127
- continue
137
+ # Choose generator g
138
+ for _ in range(1000): # try max 1000 random g
139
+ g = random.randint(2, n - 1)
128
140
 
129
- # choose a generator g
130
- g = random.randint(2, n)
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
- # the order of g modulo n must be phi/4
142
- if pow(g, int(phi / 4), n) != 1:
143
- continue
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
- logger.debug(f"n bits is {len(bin(n)[2:])}")
157
-
158
- keys["public_key"]["g"] = g
159
- keys["public_key"]["n"] = n
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
- keys["private_key"]["p"] = p
164
- keys["private_key"]["q"] = q
165
- keys["private_key"]["phi"] = phi
166
- keys["private_key"]["prime_set"] = prime_set
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
- return keys
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 / prime), n)
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) / prime), n):
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 multiply_by_contant(self, ciphertext: int, constant: int) -> int:
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 multiply_by_contant(self, ciphertext: int, constant: int) -> int:
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 multiply_by_contant(self, ciphertext: int, constant: int) -> int:
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.multiply_by_contant(ciphertext=self.value, constant=other)
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.multiply_by_contant(
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(self, key_size: int, s: Optional[int] = None) -> dict:
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(f"{self.get_algorithm_name()} is not homomorphic with respect to the addition")
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(f"{self.get_algorithm_name()} is not homomorphic with respect to the multiplication")
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(f"{self.get_algorithm_name()} is not homomorphic with respect to the exclusive or")
51
+ raise ValueError(
52
+ f"{self.get_algorithm_name()} is not homomorphic with respect to the exclusive or"
53
+ )
46
54
 
47
- def multiply_by_contant(self, ciphertext: Union[int, tuple, list], constant: int) -> int:
48
- raise ValueError(f"{self.get_algorithm_name()} is not supporting multiplying ciphertext by a known constant")
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(algorithm_name, str), f"Algorithm name for {class_name} is not defined"
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.multiply_by_contant(
271
+ dividend = self.cs.multiply_by_constant(
272
272
  ciphertext=alpha_tensor.dividend, constant=other
273
273
  )
274
- abs_dividend = self.cs.multiply_by_contant(
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.15
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
  [![YouTube](https://img.shields.io/:youtube-@sefiks-red.svg?style=flat&logo=youtube)](https://www.youtube.com/@sefiks?sub_confirmation=1)
29
29
  [![Twitter](https://img.shields.io/:follow-@serengil-blue.svg?style=flat&logo=x)](https://twitter.com/intent/user?screen_name=serengil)
30
30
 
31
- [![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)
31
+ [![Patreon](https://img.shields.io/:become-patron-f96854.svg?style=flat&logo=patreon)](https://www.patreon.com/serengil?repo=lightphe)
32
32
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/serengil?logo=GitHub&color=lightgray)](https://github.com/sponsors/serengil)
33
33
  [![Buy Me a Coffee](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](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%" height="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%" height="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.
@@ -0,0 +1,4 @@
1
+ sympy>=1.12
2
+ pytest>=7.1.2
3
+ tqdm
4
+ lightecc
@@ -0,0 +1,4 @@
1
+ sympy>=1.12
2
+ pytest>=7.1.2
3
+ tqdm
4
+ lightecc
@@ -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.15",
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.multiply_by_contant(c1, m2)) == (m1 * m2) % bn.plaintext_modulo
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.multiply_by_contant(c1, m2)) == (m1 * m2) % dg.plaintext_modulo
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.multiply_by_contant(c1, m2)
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.multiply_by_contant(c1, m2))
57
+ additive_eg.decrypt(additive_eg.multiply_by_constant(c1, m2))
58
58
  == (m1 * m2) % additive_eg.plaintext_modulo
59
59
  )
60
60
 
@@ -73,7 +73,7 @@ def __test_elliptic_curve_elgamal():
73
73
 
74
74
  # homomorphic operations
75
75
  c3 = cs.add(c1, c2)
76
- c4 = cs.multiply_by_contant(c1, m2)
76
+ c4 = cs.multiply_by_constant(c1, m2)
77
77
 
78
78
  assert cs.decrypt(c3) == m1 + m2
79
79
  assert cs.decrypt(c4) == m1 * m2
@@ -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.multiply_by_contant(c1, c2)
34
+ cs.multiply_by_constant(c1, c2)
33
35
 
34
- logger.info("✅ Goldwasser-Micali test succeeded")
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
- onprem_cs = LightPHE(algorithm_name=algorithm_name)
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 tesst succeeded for {algorithm_name}")
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.multiply_by_contant(c1, m2)) == (m1 * m2) % cs.plaintext_modulo
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.multiply_by_contant(c1, m2)) == (m1 * m2) % pai.plaintext_modulo
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):
@@ -28,7 +28,7 @@ def test_rsa():
28
28
  rsa.xor(c1, c2)
29
29
 
30
30
  with pytest.raises(ValueError):
31
- rsa.multiply_by_contant(c1, c2)
31
+ rsa.multiply_by_constant(c1, c2)
32
32
 
33
33
  with pytest.raises(ValueError):
34
34
  rsa.reencrypt(c1)
@@ -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.multiply_by_contant(new_salary_encrypted, ratio)
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")
@@ -1,4 +0,0 @@
1
- sympy==1.12
2
- pytest==7.1.2
3
- tqdm
4
- lightecc
@@ -1,4 +0,0 @@
1
- sympy==1.12
2
- pytest==7.1.2
3
- tqdm
4
- lightecc
@@ -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