polyhe 1.0.0__py3-none-any.whl
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.
- polyhe/__init__.py +31 -0
- polyhe/core/__init__.py +80 -0
- polyhe/core/ciphertext.py +115 -0
- polyhe/core/context.py +125 -0
- polyhe/openfhe.py +160 -0
- polyhe/tenseal.py +105 -0
- polyhe/test.py +50 -0
- polyhe/uarchfhe.py +127 -0
- polyhe-1.0.0.dist-info/METADATA +150 -0
- polyhe-1.0.0.dist-info/RECORD +13 -0
- polyhe-1.0.0.dist-info/WHEEL +5 -0
- polyhe-1.0.0.dist-info/licenses/LICENSE +674 -0
- polyhe-1.0.0.dist-info/top_level.txt +1 -0
polyhe/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""PolyHE package"""
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import importlib
|
|
5
|
+
|
|
6
|
+
from polyhe.core import Options
|
|
7
|
+
from polyhe.core.context import Context
|
|
8
|
+
from polyhe.core.ciphertext import Ciphertext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"new",
|
|
13
|
+
"Backend",
|
|
14
|
+
"Options",
|
|
15
|
+
"Context",
|
|
16
|
+
"Ciphertext"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Backend(enum.StrEnum):
|
|
21
|
+
"""Encryption backend"""
|
|
22
|
+
OPENFHE = enum.auto()
|
|
23
|
+
TENSEAL = enum.auto()
|
|
24
|
+
UARCHFHE = enum.auto()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def new(backend: Backend = Backend.OPENFHE, options: Options = Options()) -> Context:
|
|
28
|
+
"""Generate encryption context"""
|
|
29
|
+
module = importlib.import_module(f"{__name__}.{backend}")
|
|
30
|
+
cls: type[Context] = getattr(module, "Context")
|
|
31
|
+
return cls(options)
|
polyhe/core/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Structures package"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"Options",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# COEFF_MODULUS[security_level][poly_degree]
|
|
14
|
+
# source: sealapi.CoeffModulus.BFVDefault
|
|
15
|
+
COEFF_MODULUS = {
|
|
16
|
+
128: {
|
|
17
|
+
10: [27],
|
|
18
|
+
11: [54],
|
|
19
|
+
12: [36, 36, 37],
|
|
20
|
+
13: [43, 43, 44, 44, 44],
|
|
21
|
+
14: [48, 48, 48, 49, 49, 49, 49, 49, 49],
|
|
22
|
+
15: [55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 56]
|
|
23
|
+
},
|
|
24
|
+
192: {
|
|
25
|
+
10: [19],
|
|
26
|
+
11: [37],
|
|
27
|
+
12: [25, 25, 25],
|
|
28
|
+
13: [38, 38, 38, 38],
|
|
29
|
+
14: [50, 50, 50, 50, 50, 50],
|
|
30
|
+
15: [54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55]
|
|
31
|
+
},
|
|
32
|
+
256: {
|
|
33
|
+
10: [14],
|
|
34
|
+
11: [29],
|
|
35
|
+
12: [58],
|
|
36
|
+
13: [39, 39, 40],
|
|
37
|
+
14: [47, 47, 47, 48, 48],
|
|
38
|
+
15: [52, 53, 53, 53, 53, 53, 53, 53, 53]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(repr=False, slots=True, frozen=True)
|
|
44
|
+
class Descriptor:
|
|
45
|
+
"""Data descriptor"""
|
|
46
|
+
type: type
|
|
47
|
+
shape: tuple[int, ...]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def numpy(self) -> bool:
|
|
51
|
+
"""Is numpy-like"""
|
|
52
|
+
return issubclass(self.type, np.number)
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
"""Descriptor representation"""
|
|
56
|
+
return f"<{self.__class__.__name__} type={self.type.__name__} shape={self.shape}>"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(slots=True, frozen=True)
|
|
60
|
+
class Options:
|
|
61
|
+
"""Encryption options"""
|
|
62
|
+
slots: int = 13
|
|
63
|
+
scale: int = 40
|
|
64
|
+
security: int = 128
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def numpy2python(scalar: np.number) -> type:
|
|
68
|
+
"""Transform numpy scalar to python scalar"""
|
|
69
|
+
return type(scalar().tolist()) # type: ignore
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def materialize(obj):
|
|
73
|
+
"""Generate description and array"""
|
|
74
|
+
is_np = isinstance(obj, np.ndarray)
|
|
75
|
+
obj = np.asarray(obj)
|
|
76
|
+
type = obj.dtype.type
|
|
77
|
+
if not is_np:
|
|
78
|
+
type = numpy2python(type) # type: ignore
|
|
79
|
+
shape = obj.shape
|
|
80
|
+
return Descriptor(type, shape), obj
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Ciphertext package"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import typing
|
|
5
|
+
import operator
|
|
6
|
+
import itertools
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from polyhe.core import Descriptor
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
from polyhe.core.context import Context
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"Ciphertext",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(eq=False, order=False, slots=True, frozen=True)
|
|
23
|
+
class Ciphertext[T]:
|
|
24
|
+
"""Abstract ciphertext"""
|
|
25
|
+
_context: Context
|
|
26
|
+
_descriptor: Descriptor
|
|
27
|
+
_chunks: tuple
|
|
28
|
+
|
|
29
|
+
def _new(self, /, *args, **kwds):
|
|
30
|
+
"""Create new operable ciphertext"""
|
|
31
|
+
return self.__class__(
|
|
32
|
+
_context=self._context,
|
|
33
|
+
_descriptor=self._descriptor,
|
|
34
|
+
*args, **kwds
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _operable(self, other):
|
|
38
|
+
"""Ensure ciphertext is operable"""
|
|
39
|
+
if not isinstance(other, Ciphertext):
|
|
40
|
+
|
|
41
|
+
# Try scalar to array
|
|
42
|
+
try:
|
|
43
|
+
is_np = isinstance(other, np.ndarray)
|
|
44
|
+
other = np.full(self._descriptor.shape, other, self._descriptor.type)
|
|
45
|
+
if not is_np:
|
|
46
|
+
other = other.tolist()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# Try data to plaintext
|
|
51
|
+
try:
|
|
52
|
+
other = self._context._plaintext(other)
|
|
53
|
+
except Exception:
|
|
54
|
+
raise NotImplementedError(f"Incompatible type ({other.__class__})") # from None
|
|
55
|
+
|
|
56
|
+
if other._context.options != self._context.options:
|
|
57
|
+
raise ValueError(f"Different underlying contexts ({other._context} != {self._context})")
|
|
58
|
+
|
|
59
|
+
if other._descriptor != self._descriptor:
|
|
60
|
+
raise ValueError(f"Different underlying ciphertext ({other} != {self})")
|
|
61
|
+
|
|
62
|
+
return other
|
|
63
|
+
|
|
64
|
+
def _link(self, context):
|
|
65
|
+
"""Link ciphertext to context"""
|
|
66
|
+
|
|
67
|
+
def _neg(self, a):
|
|
68
|
+
"""Negate two ciphertext"""
|
|
69
|
+
return operator.neg(a)
|
|
70
|
+
|
|
71
|
+
def _add(self, a, b):
|
|
72
|
+
"""Add two ciphertexts"""
|
|
73
|
+
return operator.add(a, b)
|
|
74
|
+
|
|
75
|
+
def _sub(self, a, b):
|
|
76
|
+
"""Substract two ciphertexts"""
|
|
77
|
+
return operator.sub(a, b)
|
|
78
|
+
|
|
79
|
+
def _mul(self, a, b):
|
|
80
|
+
"""Multiply two ciphertexts"""
|
|
81
|
+
return operator.mul(a, b)
|
|
82
|
+
|
|
83
|
+
def _op_unary(self, op):
|
|
84
|
+
"""Execute a unary operation"""
|
|
85
|
+
self._link(self._context)
|
|
86
|
+
chunks = tuple(map(op, self._chunks))
|
|
87
|
+
return self._new(_chunks=chunks)
|
|
88
|
+
|
|
89
|
+
def _op_binary(self, op, other):
|
|
90
|
+
"""Execute a binary operation"""
|
|
91
|
+
other = self._operable(other)
|
|
92
|
+
self._link(self._context)
|
|
93
|
+
other._link(self._context)
|
|
94
|
+
chunks = tuple(itertools.starmap(op, zip(self._chunks, other._chunks)))
|
|
95
|
+
return self._new(_chunks=chunks)
|
|
96
|
+
|
|
97
|
+
def __neg__(self):
|
|
98
|
+
"""Negate a ciphertext"""
|
|
99
|
+
return self._op_unary(self._neg)
|
|
100
|
+
|
|
101
|
+
def __add__(self, other):
|
|
102
|
+
"""Add two ciphertexts"""
|
|
103
|
+
return self._op_binary(self._add, other)
|
|
104
|
+
|
|
105
|
+
def __sub__(self, other):
|
|
106
|
+
"""Substract two ciphertexts"""
|
|
107
|
+
return self._op_binary(self._sub, other)
|
|
108
|
+
|
|
109
|
+
def __mul__(self, other):
|
|
110
|
+
"""Multiply two ciphertexts"""
|
|
111
|
+
return self._op_binary(self._mul, other)
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
"""Ciphertext representation"""
|
|
115
|
+
return f"<{self.__class__.__name__} format={self._descriptor.type.__name__} shape={self._descriptor.shape}>"
|
polyhe/core/context.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Context package"""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import math
|
|
5
|
+
import typing
|
|
6
|
+
import itertools
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from polyhe.core.ciphertext import Ciphertext
|
|
14
|
+
from polyhe.core import COEFF_MODULUS, Options, materialize
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"Context",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Context(ABC):
|
|
23
|
+
"""Abstract encryption context"""
|
|
24
|
+
_cls: type[Ciphertext]
|
|
25
|
+
|
|
26
|
+
def __init__(self, options: Options = Options()) -> None:
|
|
27
|
+
"""Initialize context"""
|
|
28
|
+
self.options = options
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def _slots_exp(self) -> int:
|
|
32
|
+
"""Slot exponent"""
|
|
33
|
+
return self.options.slots
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def _slots(self) -> int:
|
|
37
|
+
"""Slot count"""
|
|
38
|
+
return 2 ** self._slots_exp
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def _scale_exp(self) -> int:
|
|
42
|
+
"""Scale exponent"""
|
|
43
|
+
return self.options.scale
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def _security_level(self) -> int:
|
|
47
|
+
"""Security level"""
|
|
48
|
+
return self.options.security
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def _poly_exp(self) -> int:
|
|
52
|
+
"""Polynomial exponent"""
|
|
53
|
+
return self._slots_exp + 1
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def _poly(self) -> int:
|
|
57
|
+
"""Polynomial size"""
|
|
58
|
+
return 2 ** self._poly_exp
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def _coeff_modulus(self) -> list[int]:
|
|
62
|
+
"""Coefficient modulus"""
|
|
63
|
+
return COEFF_MODULUS[self._security_level][self._poly_exp]
|
|
64
|
+
|
|
65
|
+
def __getstate__(self) -> dict:
|
|
66
|
+
"""Get serializable state"""
|
|
67
|
+
state: dict = dict(super().__getstate__()) # type: ignore
|
|
68
|
+
state.pop("_public", None)
|
|
69
|
+
return state
|
|
70
|
+
|
|
71
|
+
@cached_property
|
|
72
|
+
def public(self) -> typing.Self:
|
|
73
|
+
"""Get public context"""
|
|
74
|
+
return copy.deepcopy(self)
|
|
75
|
+
|
|
76
|
+
def _new(self, /, *args, **kwds) -> Ciphertext:
|
|
77
|
+
"""Create new operable ciphertext"""
|
|
78
|
+
return self._cls(_context=self.public, *args, **kwds)
|
|
79
|
+
|
|
80
|
+
def _partition(self, array: np.ndarray) -> Iterable[np.ndarray]:
|
|
81
|
+
"""Transform array into flat batches"""
|
|
82
|
+
parts = (array.size + self._slots - 1) // self._slots
|
|
83
|
+
space = np.zeros(parts * self._slots, dtype=np.float64)
|
|
84
|
+
space[:array.size] = array.ravel()
|
|
85
|
+
return np.split(space, parts)
|
|
86
|
+
|
|
87
|
+
def _encode(self, chunk):
|
|
88
|
+
"""Encode data to plaintext"""
|
|
89
|
+
return chunk
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def _encrypt(self, chunk):
|
|
93
|
+
"""Encrypt plaintext to ciphertext"""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def _decrypt(self, chunk):
|
|
97
|
+
"""Decrypt cypertext to plaintext"""
|
|
98
|
+
|
|
99
|
+
def _decode(self, chunk):
|
|
100
|
+
"""Decode plaintext to data"""
|
|
101
|
+
return chunk
|
|
102
|
+
|
|
103
|
+
def _plaintext[T](self, data: T) -> Ciphertext[T]:
|
|
104
|
+
"""Encode data to plaintext"""
|
|
105
|
+
descriptor, obj = materialize(data)
|
|
106
|
+
chunks = tuple(map(self._encode, self._partition(obj)))
|
|
107
|
+
return Ciphertext(_context=self.public, _descriptor=descriptor, _chunks=chunks)
|
|
108
|
+
|
|
109
|
+
def encrypt[T](self, data: T) -> Ciphertext[T]:
|
|
110
|
+
"""Encrypt data to ciphertext"""
|
|
111
|
+
descriptor, obj = materialize(data)
|
|
112
|
+
chunks = tuple(map(self._encrypt, map(self._encode, self._partition(obj))))
|
|
113
|
+
return self._new(_descriptor=descriptor, _chunks=chunks)
|
|
114
|
+
|
|
115
|
+
def decrypt[T](self, data: Ciphertext[T]) -> T:
|
|
116
|
+
"""Decrypt cypertext to data"""
|
|
117
|
+
data._link(self)
|
|
118
|
+
descriptor = data._descriptor
|
|
119
|
+
result = itertools.chain.from_iterable(map(self._decode, map(self._decrypt, data._chunks)))
|
|
120
|
+
result = np.fromiter(iter=result, dtype=descriptor.type, count=math.prod(descriptor.shape)).reshape(descriptor.shape)
|
|
121
|
+
return result if descriptor.numpy else result.tolist() # type: ignore
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
"""Context representation"""
|
|
125
|
+
return f"{self.__class__.__name__}(options={self.options!r})"
|
polyhe/openfhe.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""OpenFHE encryption"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typing
|
|
5
|
+
import copyreg
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
|
|
9
|
+
from polyhe import core
|
|
10
|
+
from polyhe.core import context
|
|
11
|
+
from polyhe.core import ciphertext
|
|
12
|
+
|
|
13
|
+
# Make sure global package is not confused with current package
|
|
14
|
+
_pkg = sys.path.pop(0)
|
|
15
|
+
try:
|
|
16
|
+
import openfhe
|
|
17
|
+
finally:
|
|
18
|
+
sys.path.insert(0, _pkg)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
"Context",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SECURITY_LEVEL = {
|
|
27
|
+
# 0: openfhe.SecurityLevel.HEStd_NotSet,
|
|
28
|
+
128: openfhe.SecurityLevel.HEStd_128_classic,
|
|
29
|
+
192: openfhe.SecurityLevel.HEStd_192_classic,
|
|
30
|
+
256: openfhe.SecurityLevel.HEStd_256_classic
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(repr=False, eq=False, order=False, slots=True, frozen=True)
|
|
35
|
+
class Ciphertext(ciphertext.Ciphertext):
|
|
36
|
+
"""OpenFHE ciphertext"""
|
|
37
|
+
_context: "Context"
|
|
38
|
+
|
|
39
|
+
def _neg(self, a: openfhe.Ciphertext) -> openfhe.Ciphertext:
|
|
40
|
+
"""Negate a ciphertext"""
|
|
41
|
+
return self._context._context.EvalNegate(a)
|
|
42
|
+
|
|
43
|
+
def _add(self, a: openfhe.Ciphertext, b: openfhe.Ciphertext) -> openfhe.Ciphertext:
|
|
44
|
+
"""Add two ciphertexts"""
|
|
45
|
+
return self._context._context.EvalAdd(a, b)
|
|
46
|
+
|
|
47
|
+
def _sub(self, a: openfhe.Ciphertext, b: openfhe.Ciphertext) -> openfhe.Ciphertext:
|
|
48
|
+
"""Substract two ciphertexts"""
|
|
49
|
+
return self._context._context.EvalSub(a, b)
|
|
50
|
+
|
|
51
|
+
def _mul(self, a: openfhe.Ciphertext, b: openfhe.Ciphertext) -> openfhe.Ciphertext:
|
|
52
|
+
"""Multiply two ciphertexts"""
|
|
53
|
+
return self._context._context.EvalMult(a, b)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Context(context.Context):
|
|
57
|
+
"""OpenFHE context"""
|
|
58
|
+
_cls = Ciphertext
|
|
59
|
+
|
|
60
|
+
def __init__(self, options: core.Options = core.Options()) -> None:
|
|
61
|
+
"""Initialize context"""
|
|
62
|
+
super().__init__(options)
|
|
63
|
+
|
|
64
|
+
# Context
|
|
65
|
+
level = SECURITY_LEVEL[self._security_level]
|
|
66
|
+
parameters = openfhe.CCParamsCKKSRNS()
|
|
67
|
+
parameters.SetRingDim(self._poly)
|
|
68
|
+
parameters.SetSecurityLevel(level)
|
|
69
|
+
parameters.SetScalingModSize(self._scale_exp)
|
|
70
|
+
self._context = openfhe.GenCryptoContext(parameters)
|
|
71
|
+
self._context.Enable(openfhe.PKESchemeFeature.PKE)
|
|
72
|
+
self._context.Enable(openfhe.PKESchemeFeature.LEVELEDSHE)
|
|
73
|
+
|
|
74
|
+
# Keys
|
|
75
|
+
keys = self._context.KeyGen()
|
|
76
|
+
self._public_key = keys.publicKey
|
|
77
|
+
self._private_key = keys.secretKey
|
|
78
|
+
self._context.EvalMultKeyGen(self._private_key)
|
|
79
|
+
|
|
80
|
+
@cached_property
|
|
81
|
+
def public(self) -> typing.Self:
|
|
82
|
+
"""Get public context"""
|
|
83
|
+
context = super().public
|
|
84
|
+
try:
|
|
85
|
+
del context._private_key
|
|
86
|
+
except AttributeError:
|
|
87
|
+
pass
|
|
88
|
+
return context
|
|
89
|
+
|
|
90
|
+
def _encode(self, chunk):
|
|
91
|
+
"""Ecnode data to plaintext"""
|
|
92
|
+
return self._context.MakeCKKSPackedPlaintext(chunk)
|
|
93
|
+
|
|
94
|
+
def _encrypt(self, chunk: openfhe.Plaintext) -> openfhe.Ciphertext:
|
|
95
|
+
"""Encrypt plaintext to ciphertext"""
|
|
96
|
+
return self._context.Encrypt(self._public_key, chunk)
|
|
97
|
+
|
|
98
|
+
def _decrypt(self, chunk: openfhe.Ciphertext) -> openfhe.Plaintext:
|
|
99
|
+
"""Decrypt cypertext to plaintext"""
|
|
100
|
+
return self._context.Decrypt(chunk, self._private_key)
|
|
101
|
+
|
|
102
|
+
def _decode(self, chunk: openfhe.Plaintext):
|
|
103
|
+
"""Decode plaintext to data"""
|
|
104
|
+
return chunk.GetRealPackedValue()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Serialization
|
|
108
|
+
def deserialize_context(str: bytes) -> openfhe.CryptoContext:
|
|
109
|
+
"""OpenFHE context deserializer"""
|
|
110
|
+
return openfhe.DeserializeCryptoContextString(str, openfhe.BINARY)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def deserialize_ciphertext(str: bytes) -> openfhe.Ciphertext:
|
|
114
|
+
"""OpenFHE cipher text deserializer"""
|
|
115
|
+
return openfhe.DeserializeCiphertextString(str, openfhe.BINARY)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def deserialize_publickey(str: bytes) -> openfhe.PublicKey:
|
|
119
|
+
"""OpenFHE public key deserializer"""
|
|
120
|
+
return openfhe.DeserializePublicKeyString(str, openfhe.BINARY)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def deserialize_privatekey(str: bytes) -> openfhe.PrivateKey:
|
|
124
|
+
"""OpenFHE private key deserializer"""
|
|
125
|
+
return openfhe.DeserializePrivateKeyString(str, openfhe.BINARY)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Pickle support
|
|
129
|
+
def context_reducer(context: openfhe.CryptoContext):
|
|
130
|
+
"""OpenFHE context pickle reducer"""
|
|
131
|
+
cls = deserialize_context
|
|
132
|
+
args = (openfhe.Serialize(context, openfhe.BINARY),)
|
|
133
|
+
return (cls, args)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def ciphertext_reducer(ciphertext: openfhe.Ciphertext):
|
|
137
|
+
"""OpenFHE cipher text pickle reducer"""
|
|
138
|
+
cls = deserialize_ciphertext
|
|
139
|
+
args = (openfhe.Serialize(ciphertext, openfhe.BINARY),)
|
|
140
|
+
return (cls, args)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def public_key_reducer(public_key: openfhe.PublicKey):
|
|
144
|
+
"""OpenFHE public key pickle reducer"""
|
|
145
|
+
cls = deserialize_publickey
|
|
146
|
+
args = (openfhe.Serialize(public_key, openfhe.BINARY),)
|
|
147
|
+
return (cls, args)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def private_key_reducer(private_key: openfhe.PrivateKey):
|
|
151
|
+
"""OpenFHE private key pickle reducer"""
|
|
152
|
+
cls = deserialize_privatekey
|
|
153
|
+
args = (openfhe.Serialize(private_key, openfhe.BINARY),)
|
|
154
|
+
return (cls, args)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
copyreg.pickle(openfhe.CryptoContext, context_reducer)
|
|
158
|
+
copyreg.pickle(openfhe.Ciphertext, ciphertext_reducer)
|
|
159
|
+
copyreg.pickle(openfhe.PublicKey, public_key_reducer)
|
|
160
|
+
copyreg.pickle(openfhe.PrivateKey, private_key_reducer)
|
polyhe/tenseal.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""TenSEAL encryption"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typing
|
|
5
|
+
import copyreg
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
|
|
9
|
+
from polyhe import core
|
|
10
|
+
from polyhe.core import context
|
|
11
|
+
from polyhe.core import ciphertext
|
|
12
|
+
|
|
13
|
+
# Make sure global package is not confused with current package
|
|
14
|
+
_pkg = sys.path.pop(0)
|
|
15
|
+
try:
|
|
16
|
+
import tenseal
|
|
17
|
+
from tenseal import sealapi
|
|
18
|
+
from tenseal.tensors import CKKSVector
|
|
19
|
+
from tenseal.enc_context import Context as SealContext
|
|
20
|
+
finally:
|
|
21
|
+
sys.path.insert(0, _pkg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"Context",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
SECURITY_LEVEL = {
|
|
30
|
+
# 0: sealapi.SEC_LEVEL_TYPE.NONE,
|
|
31
|
+
128: sealapi.SEC_LEVEL_TYPE.TC128,
|
|
32
|
+
192: sealapi.SEC_LEVEL_TYPE.TC192,
|
|
33
|
+
256: sealapi.SEC_LEVEL_TYPE.TC256
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(repr=False, eq=False, order=False, slots=True, frozen=True)
|
|
38
|
+
class Ciphertext(ciphertext.Ciphertext):
|
|
39
|
+
"""TenSEAL ciphertext"""
|
|
40
|
+
_context: "Context"
|
|
41
|
+
|
|
42
|
+
def _link(self, context: "Context") -> None:
|
|
43
|
+
"""Link ciphertext to context"""
|
|
44
|
+
for chunk in self._chunks:
|
|
45
|
+
chunk.link_context(context._context)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Context(context.Context):
|
|
49
|
+
"""TenSEAL context"""
|
|
50
|
+
_cls = Ciphertext
|
|
51
|
+
|
|
52
|
+
def __init__(self, options: core.Options = core.Options()) -> None:
|
|
53
|
+
"""Initialize context"""
|
|
54
|
+
super().__init__(options)
|
|
55
|
+
|
|
56
|
+
# Context
|
|
57
|
+
self._context = tenseal.context(
|
|
58
|
+
scheme=tenseal.SCHEME_TYPE.CKKS,
|
|
59
|
+
poly_modulus_degree=self._poly,
|
|
60
|
+
coeff_mod_bit_sizes=self._coeff_modulus
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Keys
|
|
64
|
+
self._context.global_scale = 2 ** self._scale_exp
|
|
65
|
+
self._context.generate_galois_keys()
|
|
66
|
+
self._context.generate_relin_keys()
|
|
67
|
+
|
|
68
|
+
@cached_property
|
|
69
|
+
def public(self) -> typing.Self:
|
|
70
|
+
"""Get public context"""
|
|
71
|
+
context = super().public
|
|
72
|
+
context._context.make_context_public()
|
|
73
|
+
return context
|
|
74
|
+
|
|
75
|
+
def _encode(self, chunk) -> tenseal.PlainTensor:
|
|
76
|
+
"""Encrypt plaintext to ciphertext"""
|
|
77
|
+
return tenseal.plain_tensor(chunk)
|
|
78
|
+
|
|
79
|
+
def _encrypt(self, chunk: tenseal.PlainTensor) -> CKKSVector:
|
|
80
|
+
"""Encrypt plaintext to ciphertext"""
|
|
81
|
+
return tenseal.ckks_vector(self.public._context, chunk)
|
|
82
|
+
|
|
83
|
+
def _decrypt(self, chunk: CKKSVector):
|
|
84
|
+
"""Decrypt cypertext to list"""
|
|
85
|
+
return chunk.decrypt()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Pickle support
|
|
89
|
+
def context_reducer(context: SealContext):
|
|
90
|
+
"""TenSEAL context pickle reducer"""
|
|
91
|
+
# FIXME: Context deserialization is very slow
|
|
92
|
+
cls = context.load
|
|
93
|
+
args = (context.serialize(save_secret_key=True),)
|
|
94
|
+
return (cls, args)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ckks_vector_reducer(vector: CKKSVector):
|
|
98
|
+
"""TenSEAL CKKS vector pickle reducer"""
|
|
99
|
+
cls = vector.lazy_load
|
|
100
|
+
args = (vector.serialize(),)
|
|
101
|
+
return (cls, args)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
copyreg.pickle(SealContext, context_reducer)
|
|
105
|
+
copyreg.pickle(CKKSVector, ckks_vector_reducer) # type: ignore (wrong inferred typing)
|
polyhe/test.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""PolyHE test"""
|
|
2
|
+
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
import polyhe
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(config: Namespace) -> None:
|
|
14
|
+
ctx = polyhe.new(config.backend)
|
|
15
|
+
|
|
16
|
+
# Numbers
|
|
17
|
+
p1 = 1
|
|
18
|
+
p2 = -1
|
|
19
|
+
c1 = ctx.encrypt(p1)
|
|
20
|
+
c2 = ctx.encrypt(p2)
|
|
21
|
+
c3 = c1 * c2 + p1
|
|
22
|
+
p3 = ctx.decrypt(c3)
|
|
23
|
+
ref = 0
|
|
24
|
+
assert np.isclose(p3, ref).all(), f"numbers error; got {p3}, expect {ref}"
|
|
25
|
+
|
|
26
|
+
# Lists
|
|
27
|
+
p1 = [0.3, 0.4]
|
|
28
|
+
p2 = [0.4, 0.3]
|
|
29
|
+
c1 = ctx.encrypt(p1)
|
|
30
|
+
c2 = ctx.encrypt(p2)
|
|
31
|
+
c3 = -c1 + c2
|
|
32
|
+
p3 = ctx.decrypt(c3)
|
|
33
|
+
ref = [0.1, -0.1]
|
|
34
|
+
assert np.isclose(p3, ref).all(), f"lists error; got {p3}, expect {ref}"
|
|
35
|
+
|
|
36
|
+
# NumPy
|
|
37
|
+
p1 = np.full((2, 3, 4), 0.4, np.float64)
|
|
38
|
+
p2 = np.full((2, 3, 4), 0.3, np.float64)
|
|
39
|
+
c1 = ctx.encrypt(p1)
|
|
40
|
+
c2 = ctx.encrypt(p2)
|
|
41
|
+
c3 = c1 - c2
|
|
42
|
+
p3 = ctx.decrypt(c3)
|
|
43
|
+
ref = np.full((2, 3, 4), 0.1, np.float64)
|
|
44
|
+
assert np.isclose(p3, ref).all(), f"numpy error; got {p3}, expect {ref}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
parser = ArgumentParser(prog="polyhe-test", description="PolyHE test")
|
|
49
|
+
parser.add_argument("backend", choices=list(polyhe.Backend), help="Operational backend")
|
|
50
|
+
main(parser.parse_args())
|