lightphe 0.0.12__tar.gz → 0.0.14__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.12 → lightphe-0.0.14}/PKG-INFO +99 -25
- {lightphe-0.0.12 → lightphe-0.0.14}/README.md +98 -24
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/__init__.py +100 -67
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/EllipticCurveElGamal.py +11 -18
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/GoldwasserMicali.py +38 -11
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/OkamotoUchiyama.py +10 -4
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/EllipticCurve.py +36 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/Tensor.py +100 -14
- lightphe-0.0.14/lightphe/standard_curves/edwards.py +144 -0
- lightphe-0.0.14/lightphe/standard_curves/inventory.py +83 -0
- lightphe-0.0.14/lightphe/standard_curves/koblitz.py +433 -0
- lightphe-0.0.14/lightphe/standard_curves/weierstrass.py +936 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe.egg-info/PKG-INFO +99 -25
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe.egg-info/SOURCES.txt +7 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/setup.py +1 -1
- lightphe-0.0.14/tests/__init__.py +0 -0
- lightphe-0.0.14/tests/test_ciphertexts.py +33 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_goldwasser.py +19 -16
- lightphe-0.0.14/tests/test_keys.py +60 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_tensors.py +107 -31
- {lightphe-0.0.12 → lightphe-0.0.14}/LICENSE +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/commons/__init__.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/commons/binary_operations.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/commons/logger.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/commons/phe_utils.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/Benaloh.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/DamgardJurik.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/ElGamal.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/NaccacheStern.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/Paillier.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/RSA.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/cryptosystems/__init__.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/elliptic_curve_forms/__init__.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/elliptic_curve_forms/edwards.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/elliptic_curve_forms/koblitz.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/elliptic_curve_forms/weierstrass.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/Algorithm.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/Ciphertext.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/Curve.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/Homomorphic.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe/models/__init__.py +0 -0
- {lightphe-0.0.12/tests → lightphe-0.0.14/lightphe/standard_curves}/__init__.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe.egg-info/dependency_links.txt +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe.egg-info/requires.txt +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/lightphe.egg-info/top_level.txt +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/requirements.txt +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/setup.cfg +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_benaloh.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_cloud.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_damgard.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_elgamal.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_ellipticcurve.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_ellipticcurveelgamal.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_exporting_keys.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_naccache.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_okamoto.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_paillier.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_rsa.py +0 -0
- {lightphe-0.0.12 → lightphe-0.0.14}/tests/test_salary.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.14
|
|
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
|
|
@@ -23,8 +23,6 @@ License-File: LICENSE
|
|
|
23
23
|
[](https://github.com/serengil/LightPHE/actions/workflows/tests.yml)
|
|
24
24
|
[](https://github.com/serengil/LightPHE/blob/master/LICENSE)
|
|
25
25
|
[](https://arxiv.org/abs/2408.05219)
|
|
26
|
-
[](https://news.ycombinator.com/item?id=43024283)
|
|
27
|
-
<!-- [](https://arxiv.org/abs/2408.05219) -->
|
|
28
26
|
|
|
29
27
|
[](https://sefiks.com)
|
|
30
28
|
[](https://www.youtube.com/@sefiks?sub_confirmation=1)
|
|
@@ -251,27 +249,6 @@ However, if you tried to multiply ciphertexts with RSA, or xor ciphertexts with
|
|
|
251
249
|
|
|
252
250
|
ECC is a powerful public-key cryptosystem based on the algebraic structure of elliptic curves over finite fields. The library supports 3 elliptic curve forms (weierstrass, edwards and koblitz) and 100+ standard elliptic curve configurations.
|
|
253
251
|
|
|
254
|
-
Building an elliptic curve with LightPHE is very straightforward. Once the curve is initialized, you can perform operations such as point addition and scalar multiplication.
|
|
255
|
-
|
|
256
|
-
```python
|
|
257
|
-
from lightphe import ECC
|
|
258
|
-
|
|
259
|
-
forms = ["weierstrass", "edwards", "koblitz"]
|
|
260
|
-
|
|
261
|
-
# construct an elliptic curve
|
|
262
|
-
ed = ECC(form_name="edwards")
|
|
263
|
-
|
|
264
|
-
# Base Point
|
|
265
|
-
G = ed.G
|
|
266
|
-
|
|
267
|
-
_2G = G + G
|
|
268
|
-
_3G = _2G + G
|
|
269
|
-
_5G = _3G + _2G
|
|
270
|
-
|
|
271
|
-
_20G = 4 * _5G
|
|
272
|
-
_19G = _20G - G
|
|
273
|
-
```
|
|
274
|
-
|
|
275
252
|
In LightPHE, the [Elliptic Curve ElGamal](https://sefiks.com/2018/08/21/elliptic-curve-elgamal-encryption/) scheme is implemented, offering a secure and efficient homomorphic encryption option.
|
|
276
253
|
|
|
277
254
|
```python
|
|
@@ -289,11 +266,108 @@ Each curve in LightPHE has a specific order, which is carefully chosen to balanc
|
|
|
289
266
|
|
|
290
267
|
See [`curves`](https://github.com/serengil/LightPHE/tree/master/lightphe/elliptic_curve_forms) page for a list of all supported forms, curves and their details.
|
|
291
268
|
|
|
269
|
+
### Vector Embeddings and Tensors
|
|
270
|
+
|
|
271
|
+
LightPHE supports homomorphic encryption on vector embeddings and tensors. This is useful in privacy-preserving machine learning, secure aggregation, and confidential data processing.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
# build a cryptosystem
|
|
275
|
+
cs = LightPHE(algorithm_name="Paillier")
|
|
276
|
+
|
|
277
|
+
# define plain embedding
|
|
278
|
+
tensor = [1.005, 2.05, 3.005, 4.005, -5.05, 6, 7.003, 7.002]
|
|
279
|
+
|
|
280
|
+
# encrypt vector embedding
|
|
281
|
+
encrypted_tensors = cs.encrypt(tensor)
|
|
282
|
+
|
|
283
|
+
# restore embedding
|
|
284
|
+
decrypted_tensors = cs.decrypt(encrypted_tensors)
|
|
285
|
+
|
|
286
|
+
# proof of work
|
|
287
|
+
assert all(abs(original - decrypted) < 1e-2 for original, decrypted in zip(tensor, decrypted_tensors))
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Encrypted embeddings retain their homomorphic properties, enabling secure computations on encrypted data without decryption. For example, two embeddings encrypted using a multiplicatively homomorphic algorithm can be multiplied element-wise.
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
# build a multiplicatively homomorphic cryptosystem (e.g. RSA)
|
|
294
|
+
cs = LightPHE("RSA")
|
|
295
|
+
|
|
296
|
+
# define plain embeddings
|
|
297
|
+
t1 = [1.005, 2.05, -3.5, 3.1, -4]
|
|
298
|
+
t2 = [5, 6.2, -7.002, -7.1, 8.02]
|
|
299
|
+
|
|
300
|
+
# encrypt embeddings
|
|
301
|
+
c1, c2 = cs.encrypt(t1), cs.encrypt(t2)
|
|
302
|
+
|
|
303
|
+
# perform element-wise homomorphic multiplication
|
|
304
|
+
c3 = c1 * c2
|
|
305
|
+
|
|
306
|
+
# proof of work
|
|
307
|
+
assert np.allclose(cs.decrypt(c3), [a * b for a, b in zip(t1, t2)], rtol=1e-2)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Similarly, two embeddings encrypted with an additively homomorphic algorithm can be added. Additionally, an encrypted embedding can be multiplied by a constant or a plain embedding.
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
# build an additively homomorphic cryptosystem (e.g. Paillier)
|
|
314
|
+
cs = LightPHE("Paillier")
|
|
315
|
+
|
|
316
|
+
# define plain embeddings
|
|
317
|
+
t1 = [1.005, 2.05, 3.6, 4, 4.02, 3.5]
|
|
318
|
+
t2 = [5, 6.2, 7.5, 8.02, 8.02, 4.5]
|
|
319
|
+
t3 = [1.03, 2.04, 3.05, 7.02, 2.01, 1.06]
|
|
320
|
+
|
|
321
|
+
# encrypt embeddings
|
|
322
|
+
c1, c2 = cs.encrypt(t1), cs.encrypt(t2)
|
|
323
|
+
|
|
324
|
+
# perform addition of two encrypted embeddings
|
|
325
|
+
c4 = c1 + c2
|
|
326
|
+
|
|
327
|
+
# perform scalar multiplication on an embedding
|
|
328
|
+
c5 = 3 * c1
|
|
329
|
+
|
|
330
|
+
# perform element-wise multiplication between an encrypted embedding and plain embedding
|
|
331
|
+
c6 = c1 * t3 # Encrypted-plaintext multiplication
|
|
332
|
+
|
|
333
|
+
# proof of work
|
|
334
|
+
assert np.allclose(cs.decrypt(c4), [a + b for a, b in zip(t1, t2)], rtol=1e-2)
|
|
335
|
+
assert np.allclose(cs.decrypt(c5), [a * 3 for a in t1], rtol=1e-2)
|
|
336
|
+
assert np.allclose(cs.decrypt(c6), [a * b for a, b in zip(t1, t3)], rtol=1e-2)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Vector Similarity Search with PHE
|
|
340
|
+
|
|
341
|
+
Many machine learning models rely on a two-tower architecture, including facial recognition, reverse image search, recommendation engines, large language models, and more. In this setup, user and item inputs are separately mapped to vector embeddings.
|
|
342
|
+
|
|
343
|
+
For example, suppose all facial embeddings in your database are encrypted on-prem in advance. When verifying an identity, the attempted facial embedding is generated on an edge device or in the cloud. You can compute the encrypted similarity by performing a dot product between the encrypted vector and the plain vector, ensuring secure comparison without decrypting sensitive data. Only additively homomorphic cryptosystems offer encrypted similarity calculation.
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
# build an additively homomorphic cryptosystem (e.g. Paillier)
|
|
347
|
+
cs = LightPHE("Paillier")
|
|
348
|
+
|
|
349
|
+
# define plain vectors - suppose those are l2 normalized already
|
|
350
|
+
alpha = [7.1, 5.2, 5.3, 2.4, 3.5, 4.6] # On-prem vector (user tower)
|
|
351
|
+
beta = [5.6, 3.7, 2.8, 4, 0, 5.9] # Cloud vector (item tower)
|
|
352
|
+
|
|
353
|
+
# encrypt embedding
|
|
354
|
+
encrypted_alpha = cs.encrypt(alpha)
|
|
355
|
+
|
|
356
|
+
# dot product of encrypted embedding and plain embedding
|
|
357
|
+
encrypted_cosine_similarity = encrypted_alpha @ beta
|
|
358
|
+
|
|
359
|
+
# decrypt similarity
|
|
360
|
+
cosine_similarity = cs.decrypt(encrypted_cosine_similarity)[0]
|
|
361
|
+
|
|
362
|
+
# proof of work
|
|
363
|
+
assert abs(cosine_similarity - sum(x * y for x, y in zip(alpha, beta))) < 1e-2
|
|
364
|
+
```
|
|
365
|
+
|
|
292
366
|
# Contributing
|
|
293
367
|
|
|
294
368
|
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.
|
|
295
369
|
|
|
296
|
-
You should be able run `make test` and `make lint` commands successfully before committing. Once a PR is created, GitHub test workflow will be run automatically and unit test results will be available in [GitHub actions](https://github.com/serengil/LightPHE/actions/workflows/tests.yml) before approval.
|
|
370
|
+
You should be able run `make test` and `make lint` commands successfully before committing. Once a PR is created, GitHub test workflow will be run automatically and unit test results will be available in [GitHub actions](https://github.com/serengil/LightPHE/actions/workflows/tests.yml) before approval.
|
|
297
371
|
|
|
298
372
|
# Support
|
|
299
373
|
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
[](https://github.com/serengil/LightPHE/actions/workflows/tests.yml)
|
|
8
8
|
[](https://github.com/serengil/LightPHE/blob/master/LICENSE)
|
|
9
9
|
[](https://arxiv.org/abs/2408.05219)
|
|
10
|
-
[](https://news.ycombinator.com/item?id=43024283)
|
|
11
|
-
<!-- [](https://arxiv.org/abs/2408.05219) -->
|
|
12
10
|
|
|
13
11
|
[](https://sefiks.com)
|
|
14
12
|
[](https://www.youtube.com/@sefiks?sub_confirmation=1)
|
|
@@ -235,27 +233,6 @@ However, if you tried to multiply ciphertexts with RSA, or xor ciphertexts with
|
|
|
235
233
|
|
|
236
234
|
ECC is a powerful public-key cryptosystem based on the algebraic structure of elliptic curves over finite fields. The library supports 3 elliptic curve forms (weierstrass, edwards and koblitz) and 100+ standard elliptic curve configurations.
|
|
237
235
|
|
|
238
|
-
Building an elliptic curve with LightPHE is very straightforward. Once the curve is initialized, you can perform operations such as point addition and scalar multiplication.
|
|
239
|
-
|
|
240
|
-
```python
|
|
241
|
-
from lightphe import ECC
|
|
242
|
-
|
|
243
|
-
forms = ["weierstrass", "edwards", "koblitz"]
|
|
244
|
-
|
|
245
|
-
# construct an elliptic curve
|
|
246
|
-
ed = ECC(form_name="edwards")
|
|
247
|
-
|
|
248
|
-
# Base Point
|
|
249
|
-
G = ed.G
|
|
250
|
-
|
|
251
|
-
_2G = G + G
|
|
252
|
-
_3G = _2G + G
|
|
253
|
-
_5G = _3G + _2G
|
|
254
|
-
|
|
255
|
-
_20G = 4 * _5G
|
|
256
|
-
_19G = _20G - G
|
|
257
|
-
```
|
|
258
|
-
|
|
259
236
|
In LightPHE, the [Elliptic Curve ElGamal](https://sefiks.com/2018/08/21/elliptic-curve-elgamal-encryption/) scheme is implemented, offering a secure and efficient homomorphic encryption option.
|
|
260
237
|
|
|
261
238
|
```python
|
|
@@ -273,11 +250,108 @@ Each curve in LightPHE has a specific order, which is carefully chosen to balanc
|
|
|
273
250
|
|
|
274
251
|
See [`curves`](https://github.com/serengil/LightPHE/tree/master/lightphe/elliptic_curve_forms) page for a list of all supported forms, curves and their details.
|
|
275
252
|
|
|
253
|
+
### Vector Embeddings and Tensors
|
|
254
|
+
|
|
255
|
+
LightPHE supports homomorphic encryption on vector embeddings and tensors. This is useful in privacy-preserving machine learning, secure aggregation, and confidential data processing.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
# build a cryptosystem
|
|
259
|
+
cs = LightPHE(algorithm_name="Paillier")
|
|
260
|
+
|
|
261
|
+
# define plain embedding
|
|
262
|
+
tensor = [1.005, 2.05, 3.005, 4.005, -5.05, 6, 7.003, 7.002]
|
|
263
|
+
|
|
264
|
+
# encrypt vector embedding
|
|
265
|
+
encrypted_tensors = cs.encrypt(tensor)
|
|
266
|
+
|
|
267
|
+
# restore embedding
|
|
268
|
+
decrypted_tensors = cs.decrypt(encrypted_tensors)
|
|
269
|
+
|
|
270
|
+
# proof of work
|
|
271
|
+
assert all(abs(original - decrypted) < 1e-2 for original, decrypted in zip(tensor, decrypted_tensors))
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Encrypted embeddings retain their homomorphic properties, enabling secure computations on encrypted data without decryption. For example, two embeddings encrypted using a multiplicatively homomorphic algorithm can be multiplied element-wise.
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# build a multiplicatively homomorphic cryptosystem (e.g. RSA)
|
|
278
|
+
cs = LightPHE("RSA")
|
|
279
|
+
|
|
280
|
+
# define plain embeddings
|
|
281
|
+
t1 = [1.005, 2.05, -3.5, 3.1, -4]
|
|
282
|
+
t2 = [5, 6.2, -7.002, -7.1, 8.02]
|
|
283
|
+
|
|
284
|
+
# encrypt embeddings
|
|
285
|
+
c1, c2 = cs.encrypt(t1), cs.encrypt(t2)
|
|
286
|
+
|
|
287
|
+
# perform element-wise homomorphic multiplication
|
|
288
|
+
c3 = c1 * c2
|
|
289
|
+
|
|
290
|
+
# proof of work
|
|
291
|
+
assert np.allclose(cs.decrypt(c3), [a * b for a, b in zip(t1, t2)], rtol=1e-2)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Similarly, two embeddings encrypted with an additively homomorphic algorithm can be added. Additionally, an encrypted embedding can be multiplied by a constant or a plain embedding.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
# build an additively homomorphic cryptosystem (e.g. Paillier)
|
|
298
|
+
cs = LightPHE("Paillier")
|
|
299
|
+
|
|
300
|
+
# define plain embeddings
|
|
301
|
+
t1 = [1.005, 2.05, 3.6, 4, 4.02, 3.5]
|
|
302
|
+
t2 = [5, 6.2, 7.5, 8.02, 8.02, 4.5]
|
|
303
|
+
t3 = [1.03, 2.04, 3.05, 7.02, 2.01, 1.06]
|
|
304
|
+
|
|
305
|
+
# encrypt embeddings
|
|
306
|
+
c1, c2 = cs.encrypt(t1), cs.encrypt(t2)
|
|
307
|
+
|
|
308
|
+
# perform addition of two encrypted embeddings
|
|
309
|
+
c4 = c1 + c2
|
|
310
|
+
|
|
311
|
+
# perform scalar multiplication on an embedding
|
|
312
|
+
c5 = 3 * c1
|
|
313
|
+
|
|
314
|
+
# perform element-wise multiplication between an encrypted embedding and plain embedding
|
|
315
|
+
c6 = c1 * t3 # Encrypted-plaintext multiplication
|
|
316
|
+
|
|
317
|
+
# proof of work
|
|
318
|
+
assert np.allclose(cs.decrypt(c4), [a + b for a, b in zip(t1, t2)], rtol=1e-2)
|
|
319
|
+
assert np.allclose(cs.decrypt(c5), [a * 3 for a in t1], rtol=1e-2)
|
|
320
|
+
assert np.allclose(cs.decrypt(c6), [a * b for a, b in zip(t1, t3)], rtol=1e-2)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Vector Similarity Search with PHE
|
|
324
|
+
|
|
325
|
+
Many machine learning models rely on a two-tower architecture, including facial recognition, reverse image search, recommendation engines, large language models, and more. In this setup, user and item inputs are separately mapped to vector embeddings.
|
|
326
|
+
|
|
327
|
+
For example, suppose all facial embeddings in your database are encrypted on-prem in advance. When verifying an identity, the attempted facial embedding is generated on an edge device or in the cloud. You can compute the encrypted similarity by performing a dot product between the encrypted vector and the plain vector, ensuring secure comparison without decrypting sensitive data. Only additively homomorphic cryptosystems offer encrypted similarity calculation.
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
# build an additively homomorphic cryptosystem (e.g. Paillier)
|
|
331
|
+
cs = LightPHE("Paillier")
|
|
332
|
+
|
|
333
|
+
# define plain vectors - suppose those are l2 normalized already
|
|
334
|
+
alpha = [7.1, 5.2, 5.3, 2.4, 3.5, 4.6] # On-prem vector (user tower)
|
|
335
|
+
beta = [5.6, 3.7, 2.8, 4, 0, 5.9] # Cloud vector (item tower)
|
|
336
|
+
|
|
337
|
+
# encrypt embedding
|
|
338
|
+
encrypted_alpha = cs.encrypt(alpha)
|
|
339
|
+
|
|
340
|
+
# dot product of encrypted embedding and plain embedding
|
|
341
|
+
encrypted_cosine_similarity = encrypted_alpha @ beta
|
|
342
|
+
|
|
343
|
+
# decrypt similarity
|
|
344
|
+
cosine_similarity = cs.decrypt(encrypted_cosine_similarity)[0]
|
|
345
|
+
|
|
346
|
+
# proof of work
|
|
347
|
+
assert abs(cosine_similarity - sum(x * y for x, y in zip(alpha, beta))) < 1e-2
|
|
348
|
+
```
|
|
349
|
+
|
|
276
350
|
# Contributing
|
|
277
351
|
|
|
278
352
|
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.
|
|
279
353
|
|
|
280
|
-
You should be able run `make test` and `make lint` commands successfully before committing. Once a PR is created, GitHub test workflow will be run automatically and unit test results will be available in [GitHub actions](https://github.com/serengil/LightPHE/actions/workflows/tests.yml) before approval.
|
|
354
|
+
You should be able run `make test` and `make lint` commands successfully before committing. Once a PR is created, GitHub test workflow will be run automatically and unit test results will be available in [GitHub actions](https://github.com/serengil/LightPHE/actions/workflows/tests.yml) before approval.
|
|
281
355
|
|
|
282
356
|
# Support
|
|
283
357
|
|
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
from typing import Optional, Union, List
|
|
5
5
|
import multiprocessing
|
|
6
6
|
from contextlib import closing
|
|
7
|
+
import traceback
|
|
8
|
+
import copy
|
|
7
9
|
|
|
8
10
|
# 3rd party dependencies
|
|
9
11
|
from tqdm import tqdm
|
|
@@ -36,7 +38,7 @@ from lightphe.elliptic_curve_forms.koblitz import Koblitz
|
|
|
36
38
|
|
|
37
39
|
logger = Logger(module="lightphe/__init__.py")
|
|
38
40
|
|
|
39
|
-
VERSION = "0.0.
|
|
41
|
+
VERSION = "0.0.14"
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class LightPHE:
|
|
@@ -155,12 +157,13 @@ class LightPHE:
|
|
|
155
157
|
return cs
|
|
156
158
|
|
|
157
159
|
def encrypt(
|
|
158
|
-
self, plaintext: Union[int, float, list]
|
|
160
|
+
self, plaintext: Union[int, float, list], silent: bool = False
|
|
159
161
|
) -> Union[Ciphertext, EncryptedTensor]:
|
|
160
162
|
"""
|
|
161
163
|
Encrypt a plaintext with a built cryptosystem
|
|
162
164
|
Args:
|
|
163
165
|
plaintext (int, float or tensor): message
|
|
166
|
+
silent (bool): set this to True if you do not want to see progress bar
|
|
164
167
|
Returns
|
|
165
168
|
ciphertext (from lightphe.models.Ciphertext import Ciphertext): encrypted message
|
|
166
169
|
"""
|
|
@@ -169,16 +172,21 @@ class LightPHE:
|
|
|
169
172
|
|
|
170
173
|
if isinstance(plaintext, list):
|
|
171
174
|
# then encrypt tensors
|
|
172
|
-
return self.__encrypt_tensors(tensor=plaintext)
|
|
175
|
+
return self.__encrypt_tensors(tensor=plaintext, silent=silent)
|
|
173
176
|
|
|
174
177
|
ciphertext = self.cs.encrypt(
|
|
175
178
|
plaintext=phe_utils.normalize_input(
|
|
176
179
|
value=plaintext, modulo=self.cs.plaintext_modulo
|
|
177
180
|
)
|
|
178
181
|
)
|
|
182
|
+
|
|
183
|
+
public_keys = self.cs.keys.copy()
|
|
184
|
+
if public_keys.get("private_key") is not None:
|
|
185
|
+
del public_keys["private_key"]
|
|
186
|
+
|
|
179
187
|
return Ciphertext(
|
|
180
188
|
algorithm_name=self.algorithm_name,
|
|
181
|
-
keys=
|
|
189
|
+
keys=public_keys,
|
|
182
190
|
value=ciphertext,
|
|
183
191
|
form=self.form,
|
|
184
192
|
curve=self.curve,
|
|
@@ -206,11 +214,12 @@ class LightPHE:
|
|
|
206
214
|
|
|
207
215
|
return self.cs.decrypt(ciphertext=ciphertext.value)
|
|
208
216
|
|
|
209
|
-
def __encrypt_tensors(self, tensor: list) -> EncryptedTensor:
|
|
217
|
+
def __encrypt_tensors(self, tensor: list, silent: bool = False) -> EncryptedTensor:
|
|
210
218
|
"""
|
|
211
219
|
Encrypt a given tensor
|
|
212
220
|
Args:
|
|
213
221
|
tensor (list of int or float)
|
|
222
|
+
silent (bool): set this to True if you do not want to see progress bar
|
|
214
223
|
Returns
|
|
215
224
|
encrypted tensor (list of encrypted tensor object)
|
|
216
225
|
"""
|
|
@@ -218,7 +227,6 @@ class LightPHE:
|
|
|
218
227
|
|
|
219
228
|
encrypted_zero = self.cs.encrypt(plaintext=0)
|
|
220
229
|
divisor_encrypted = self.cs.encrypt(plaintext=10**self.precision)
|
|
221
|
-
is_positive_tensor = all(m >= 0 for m in tensor)
|
|
222
230
|
|
|
223
231
|
num_workers = min(len(tensor), 2 * multiprocessing.cpu_count())
|
|
224
232
|
logger.debug(f"encrypting tensors in {num_workers} parallel")
|
|
@@ -228,14 +236,13 @@ class LightPHE:
|
|
|
228
236
|
|
|
229
237
|
for m in tensor:
|
|
230
238
|
f = pool.apply_async(
|
|
231
|
-
|
|
239
|
+
encrypt_float,
|
|
232
240
|
(
|
|
233
241
|
m,
|
|
234
242
|
divisor_encrypted,
|
|
235
243
|
self.cs,
|
|
236
244
|
self.precision,
|
|
237
245
|
encrypted_zero,
|
|
238
|
-
is_positive_tensor,
|
|
239
246
|
),
|
|
240
247
|
)
|
|
241
248
|
funclist.append(f)
|
|
@@ -245,7 +252,7 @@ class LightPHE:
|
|
|
245
252
|
for f in tqdm(
|
|
246
253
|
funclist,
|
|
247
254
|
desc="Encrypting tensors",
|
|
248
|
-
disable=
|
|
255
|
+
disable=silent,
|
|
249
256
|
):
|
|
250
257
|
result = f.get(timeout=10)
|
|
251
258
|
encrypted_tensor.append(result)
|
|
@@ -253,9 +260,13 @@ class LightPHE:
|
|
|
253
260
|
toc = time.time()
|
|
254
261
|
logger.debug(f"encryption took {toc - tic} seconds")
|
|
255
262
|
|
|
263
|
+
public_cs = copy.deepcopy(self.cs)
|
|
264
|
+
if public_cs.keys.get("private_key") is not None:
|
|
265
|
+
del public_cs.keys["private_key"]
|
|
266
|
+
|
|
256
267
|
return EncryptedTensor(
|
|
257
268
|
fractions=encrypted_tensor,
|
|
258
|
-
cs=
|
|
269
|
+
cs=public_cs,
|
|
259
270
|
precision=self.precision,
|
|
260
271
|
)
|
|
261
272
|
|
|
@@ -277,7 +288,10 @@ class LightPHE:
|
|
|
277
288
|
sign = c.sign
|
|
278
289
|
abs_dividend = self.cs.decrypt(ciphertext=c.abs_dividend)
|
|
279
290
|
# dividend = self.cs.decrypt(ciphertext=c.dividend)
|
|
291
|
+
|
|
292
|
+
# TODO: do I really need encrypted divisor? cannot I store current_precision
|
|
280
293
|
divisor = self.cs.decrypt(ciphertext=c.divisor)
|
|
294
|
+
|
|
281
295
|
m = sign * abs_dividend / divisor
|
|
282
296
|
|
|
283
297
|
plain_tensor.append(m)
|
|
@@ -365,72 +379,91 @@ class LightPHE:
|
|
|
365
379
|
)
|
|
366
380
|
|
|
367
381
|
|
|
368
|
-
def
|
|
382
|
+
def encrypt_float(
|
|
369
383
|
m: Union[int, float],
|
|
370
|
-
divisor_encrypted,
|
|
384
|
+
divisor_encrypted: int,
|
|
371
385
|
cs: Homomorphic,
|
|
372
386
|
precision: int,
|
|
373
387
|
encrypted_zero: int,
|
|
374
|
-
is_positive_tensor: bool,
|
|
375
388
|
) -> Fraction:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
389
|
+
"""
|
|
390
|
+
Encrypt a float value
|
|
391
|
+
Args:
|
|
392
|
+
m (int or float): message to encrypt
|
|
393
|
+
divisor_encrypted (int): pre-calculated encrypted divisor
|
|
394
|
+
cs (Homomorphic): cryptosystem itself
|
|
395
|
+
precision (int): define how many digits after dot
|
|
396
|
+
encrypted_zero (int): pre-calculated encrypted value of 0
|
|
397
|
+
Returns:
|
|
398
|
+
result (Fraction): encrypted float value
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
if m == 0:
|
|
402
|
+
# this is very common in VGG-Face embeddings
|
|
403
|
+
c = Fraction(
|
|
404
|
+
dividend=encrypted_zero,
|
|
405
|
+
divisor=divisor_encrypted,
|
|
406
|
+
abs_dividend=encrypted_zero,
|
|
407
|
+
sign=1,
|
|
393
408
|
)
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
elif isinstance(m, int):
|
|
410
|
+
dividend_encrypted = cs.encrypt(
|
|
411
|
+
plaintext=(m % cs.plaintext_modulo) * pow(10, precision)
|
|
412
|
+
)
|
|
413
|
+
abs_dividend_encrypted = (
|
|
414
|
+
dividend_encrypted
|
|
415
|
+
if m > 0
|
|
416
|
+
else cs.encrypt(
|
|
417
|
+
plaintext=(abs(m) % cs.plaintext_modulo) * pow(10, precision)
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
# divisor_encrypted = self.cs.encrypt(plaintext=pow(10, self.precision))
|
|
421
|
+
c = Fraction(
|
|
422
|
+
dividend=dividend_encrypted,
|
|
423
|
+
divisor=divisor_encrypted,
|
|
424
|
+
abs_dividend=abs_dividend_encrypted,
|
|
425
|
+
sign=1 if m >= 0 else -1,
|
|
426
|
+
)
|
|
427
|
+
elif isinstance(m, float):
|
|
428
|
+
# got `int too large to convert float` while m mod plaintext modulo
|
|
429
|
+
# when security level is set to 128
|
|
430
|
+
dividend, _ = phe_utils.fractionize(
|
|
431
|
+
value=(m % cs.plaintext_modulo if m > cs.plaintext_modulo else m),
|
|
413
432
|
modulo=cs.plaintext_modulo,
|
|
414
433
|
precision=precision,
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
434
|
+
)
|
|
435
|
+
abs_dividend = (
|
|
436
|
+
dividend
|
|
437
|
+
if m > 0
|
|
438
|
+
else phe_utils.fractionize(
|
|
439
|
+
value=(
|
|
440
|
+
abs(m) % cs.plaintext_modulo
|
|
441
|
+
if abs(m) > cs.plaintext_modulo
|
|
442
|
+
else abs(m)
|
|
443
|
+
),
|
|
444
|
+
modulo=cs.plaintext_modulo,
|
|
445
|
+
precision=precision,
|
|
446
|
+
)[0]
|
|
447
|
+
)
|
|
448
|
+
dividend_encrypted = cs.encrypt(plaintext=dividend)
|
|
449
|
+
abs_dividend_encrypted = (
|
|
450
|
+
dividend_encrypted if m > 0 else cs.encrypt(plaintext=abs_dividend)
|
|
451
|
+
)
|
|
452
|
+
# divisor_encrypted = self.cs.encrypt(plaintext=_divisor)
|
|
453
|
+
c = Fraction(
|
|
454
|
+
dividend=dividend_encrypted,
|
|
455
|
+
divisor=divisor_encrypted,
|
|
456
|
+
abs_dividend=abs_dividend_encrypted,
|
|
457
|
+
sign=1 if m >= 0 else -1,
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
raise ValueError(f"unimplemented type - {type(m)}")
|
|
432
461
|
|
|
433
|
-
|
|
462
|
+
return c
|
|
463
|
+
except Exception as err:
|
|
464
|
+
logger.error(f"Exception while running encrypt_float: {str(err)}")
|
|
465
|
+
logger.error(traceback.format_exc())
|
|
466
|
+
raise err
|
|
434
467
|
|
|
435
468
|
|
|
436
469
|
class ECC:
|
|
@@ -46,8 +46,13 @@ class EllipticCurveElGamal(Homomorphic):
|
|
|
46
46
|
self.ecc = ECC(form_name=form, curve_name=curve)
|
|
47
47
|
|
|
48
48
|
self.keys = keys or self.generate_keys(key_size or self.ecc.n.bit_length())
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
|
|
50
|
+
if curve is not None:
|
|
51
|
+
self.keys["curve"] = curve
|
|
52
|
+
|
|
53
|
+
if form is not None:
|
|
54
|
+
self.keys["form"] = form
|
|
55
|
+
|
|
51
56
|
self.plaintext_modulo = self.ecc.modulo
|
|
52
57
|
self.ciphertext_modulo = self.ecc.modulo
|
|
53
58
|
|
|
@@ -74,7 +79,7 @@ class EllipticCurveElGamal(Homomorphic):
|
|
|
74
79
|
# public key
|
|
75
80
|
Qa = self.ecc.G * ka
|
|
76
81
|
|
|
77
|
-
keys["public_key"]["Qa"] = Qa
|
|
82
|
+
keys["public_key"]["Qa"] = Qa.get_point()
|
|
78
83
|
keys["private_key"]["ka"] = ka
|
|
79
84
|
|
|
80
85
|
return keys
|
|
@@ -98,7 +103,8 @@ class EllipticCurveElGamal(Homomorphic):
|
|
|
98
103
|
ciphertext (tuple): c1 and c2
|
|
99
104
|
"""
|
|
100
105
|
# public key
|
|
101
|
-
|
|
106
|
+
x, y = self.keys["public_key"]["Qa"]
|
|
107
|
+
Qa = EllipticCurvePoint(x=x, y=y, curve=self.ecc.curve)
|
|
102
108
|
|
|
103
109
|
# random key
|
|
104
110
|
r = random_key or self.generate_random_key()
|
|
@@ -134,20 +140,7 @@ class EllipticCurveElGamal(Homomorphic):
|
|
|
134
140
|
# we need to find k from known s_prime and G
|
|
135
141
|
# this requires to solve ECDLP
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
G = self.ecc.G
|
|
139
|
-
k = 2
|
|
140
|
-
while True:
|
|
141
|
-
G = G + self.ecc.G
|
|
142
|
-
|
|
143
|
-
if G == s_prime:
|
|
144
|
-
return k
|
|
145
|
-
|
|
146
|
-
k = k + 1
|
|
147
|
-
if k > self.ecc.n:
|
|
148
|
-
raise ValueError(
|
|
149
|
-
f"Cannot restore scalar from {s_prime} = k x {self.ecc.G}"
|
|
150
|
-
)
|
|
143
|
+
return s_prime / self.ecc.G
|
|
151
144
|
|
|
152
145
|
def multiply(self, ciphertext1: tuple, ciphertext2: tuple) -> tuple:
|
|
153
146
|
raise ValueError(
|