diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml deleted file mode 100644 index 43628a9..0000000 --- a/.github/workflows/python-package.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python package - -on: - push: - branches: [ 3.3.2 ] - pull_request: - branches: [ 3.3.2 ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.5, 3.6, 3.7, 3.8] - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - sudo apt-get install python3 python3-pip build-essential cmake - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: build seal - run: | - cd SEAL/native/src - cmake . - make - - name: build seal-python - run: | - python3 setup.py build_ext -i - python3 setup.py install - - diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..de40d93 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,111 @@ +name: Build and Publish Wheels + +on: + pull_request: + push: + branches: [main] + tags: ["v*"] + release: + types: [published] + workflow_dispatch: + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + - os: windows-latest + - os: macos-14 + - os: macos-15 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build wheels + uses: pypa/cibuildwheel@v2.22.0 + env: + CIBW_BUILD_VERBOSITY: "1" + CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*" + CIBW_SKIP: "*-musllinux_* pp* cp38-macosx_arm64" + CIBW_ARCHS_LINUX: "x86_64" + CIBW_ARCHS_WINDOWS: "AMD64" + CIBW_ARCHS_MACOS: "auto64" + CIBW_TEST_COMMAND: "python -c \"import seal; print(seal.__version__)\"" + CIBW_BEFORE_BUILD: > + python -m pip install --upgrade pip cmake && + cmake -S SEAL -B SEAL/build -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF -DSEAL_USE_ZSTD=OFF && + cmake --build SEAL/build --config Release && + python -c "import glob; print('SEAL libs:', glob.glob('SEAL/build/**/*.lib', recursive=True)[:50])" + + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + + build_sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: python -m pip install --upgrade pip build cmake + + - name: Build SEAL static libs + run: | + cmake -S SEAL -B SEAL/build -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF -DSEAL_USE_ZSTD=OFF + cmake --build SEAL/build --config Release + + - name: Build source distribution + run: python -m build --sdist + + - name: Upload sdist artifact + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + publish_pypi: + name: Publish to PyPI + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + permissions: + id-token: write + steps: + - name: Download wheel artifacts + uses: actions/download-artifact@v4 + with: + path: dist + pattern: wheels-* + merge-multiple: true + + - name: Download sdist artifact + uses: actions/download-artifact@v4 + with: + path: dist + name: sdist + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index f4a04da..a88509b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ build temp .idea *.bin +dist diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..20ac59a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +include LICENSE +include seal.pyi +include py.typed diff --git a/README.md b/README.md index 9c1971e..6758bdf 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,12 @@ This is a python binding for the Microsoft SEAL library. ## Contents * [Build](#build) +* [Typing](#typing) * [Note](#note) * [Serialize](#serialize) * [Other](#other) * [FAQ](#faq) +* [Release](#release) @@ -163,6 +165,26 @@ This is a python binding for the Microsoft SEAL library. 2. Edit `extra_objects` in setup.py to `*.dylib` or else. +## Typing + +This project now ships `seal.pyi` and `py.typed` for Python type checking (PEP 561). + +After build/install, editors such as Pylance/Pyright/mypy can use these hints for autocomplete and static analysis. + + +## Release + +Use `RELEASE.md` for the full PyPI release checklist. + +Quick commands: + +```bash +python3 setup.py sdist bdist_wheel +python3 -m twine check dist/* +python3 -m twine upload dist/* +``` + + ## Contributing diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..c941f1c --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,46 @@ +# Release to PyPI + +## 1. Build prerequisites + +```bash +python3 -m pip install --upgrade pip build twine +git submodule update --init --recursive +cmake -S SEAL -B SEAL/build -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF -DSEAL_USE_ZSTD=OFF +cmake --build SEAL/build +``` + +## 2. Build wheel and sdist + +```bash +python3 setup.py sdist bdist_wheel +``` + +## 3. Validate artifacts + +```bash +python3 -m twine check dist/* +``` + +## 4. Upload to TestPyPI (recommended first) + +```bash +python3 -m twine upload --repository testpypi dist/* +``` + +## 5. Upload to PyPI + +```bash +python3 -m twine upload dist/* +``` + +## 6. Post-upload smoke test + +```bash +python3 -m venv /tmp/seal-publish-test +source /tmp/seal-publish-test/bin/activate +python -m pip install seal-python +python - <<'PY' +import seal +print("seal version:", seal.__version__) +PY +``` diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/py.typed @@ -0,0 +1 @@ + diff --git a/seal.pyi b/seal.pyi new file mode 100644 index 0000000..4925a77 --- /dev/null +++ b/seal.pyi @@ -0,0 +1,470 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import Iterable, Sequence, TypeAlias, overload + +import numpy as np +from numpy.typing import NDArray + +__version__: str + +ParmsId: TypeAlias = Sequence[int] +FloatLikeArray: TypeAlias = Iterable[float] +ComplexLikeArray: TypeAlias = Iterable[complex] +IntLikeArray: TypeAlias = Iterable[int] + + +class scheme_type(IntEnum): + none: int + bfv: int + ckks: int + bgv: int + + +class compr_mode_type(IntEnum): + none: int + zlib: int + zstd: int + + +class sec_level_type(IntEnum): + none: int + tc128: int + tc192: int + tc256: int + + +class error_type(IntEnum): + none: int + success: int + invalid_scheme: int + invalid_coeff_modulus_size: int + invalid_coeff_modulus_bit_count: int + invalid_coeff_modulus_no_ntt: int + invalid_poly_modulus_degree: int + invalid_poly_modulus_degree_non_power_of_two: int + invalid_parameters_too_large: int + invalid_parameters_insecure: int + failed_creating_rns_base: int + invalid_plain_modulus_bit_count: int + invalid_plain_modulus_coprimality: int + invalid_plain_modulus_too_large: int + invalid_plain_modulus_nonzero: int + failed_creating_rns_tool: int + + +class VectorDouble(list[float]): ... +class VectorComplex(list[complex]): ... +class VectorUInt(list[int]): ... +class VectorInt(list[int]): ... + + +class MemoryPoolHandle: + def __init__(self) -> None: ... + @staticmethod + def Global() -> MemoryPoolHandle: ... + @staticmethod + def ThreadLocal() -> MemoryPoolHandle: ... + @staticmethod + def New(clear_on_destruction: bool = False) -> MemoryPoolHandle: ... + def pool_count(self) -> int: ... + def alloc_byte_count(self) -> int: ... + def use_count(self) -> int: ... + def is_initialized(self) -> bool: ... + + +class MemoryManager: + @staticmethod + def GetPool() -> MemoryPoolHandle: ... + + +class Modulus: + def __init__(self, value: int) -> None: ... + def bit_count(self) -> int: ... + def value(self) -> int: ... + def is_zero(self) -> bool: ... + def is_prime(self) -> bool: ... + def reduce(self, value: int) -> int: ... + + +class EncryptionParameters: + @overload + def __init__(self, scheme: scheme_type) -> None: ... + @overload + def __init__(self, other: EncryptionParameters) -> None: ... + def set_poly_modulus_degree(self, poly_modulus_degree: int) -> None: ... + def set_coeff_modulus(self, coeff_modulus: Sequence[Modulus]) -> None: ... + @overload + def set_plain_modulus(self, plain_modulus: Modulus) -> None: ... + @overload + def set_plain_modulus(self, plain_modulus: int) -> None: ... + def scheme(self) -> scheme_type: ... + def poly_modulus_degree(self) -> int: ... + def coeff_modulus(self) -> Sequence[Modulus]: ... + def plain_modulus(self) -> Modulus: ... + @overload + def save(self, path: str) -> None: ... + @overload + def save(self, path: str, compr_mode: compr_mode_type) -> None: ... + def load(self, path: str) -> None: ... + def load_bytes(self, data: bytes) -> None: ... + def save_size(self, compr_mode: compr_mode_type = ...) -> int: ... + def to_bytes(self, compr_mode: compr_mode_type = ...) -> bytes: ... + + +class EncryptionParameterQualifiers: + parameter_error: error_type + using_fft: bool + using_ntt: bool + using_batching: bool + using_fast_plain_lift: bool + using_descending_modulus_chain: bool + sec_level: sec_level_type + def parameters_set(self) -> bool: ... + def parameter_error_name(self) -> str: ... + def parameter_error_message(self) -> str: ... + + +class ContextData: + def parms(self) -> EncryptionParameters: ... + def parms_id(self) -> ParmsId: ... + def qualifiers(self) -> EncryptionParameterQualifiers: ... + def total_coeff_modulus(self) -> int: ... + def total_coeff_modulus_bit_count(self) -> int: ... + def next_context_data(self) -> ContextData | None: ... + def chain_index(self) -> int: ... + + +class SEALContext: + def __init__( + self, + parms: EncryptionParameters, + expand_mod_chain: bool = True, + sec_level: sec_level_type = sec_level_type.tc128, + ) -> None: ... + def get_context_data(self, parms_id: ParmsId) -> ContextData | None: ... + def key_context_data(self) -> ContextData: ... + def first_context_data(self) -> ContextData: ... + def last_context_data(self) -> ContextData: ... + def parameters_set(self) -> bool: ... + def parameter_error_name(self) -> str: ... + def parameter_error_message(self) -> str: ... + def first_parms_id(self) -> ParmsId: ... + def last_parms_id(self) -> ParmsId: ... + def using_keyswitching(self) -> bool: ... + def from_cipher_str(self, data: bytes | str) -> Ciphertext: ... + def from_plain_str(self, data: bytes | str) -> Plaintext: ... + def from_secret_str(self, data: bytes | str) -> SecretKey: ... + def from_public_str(self, data: bytes | str) -> PublicKey: ... + def from_relin_str(self, data: bytes | str) -> RelinKeys: ... + def from_galois_str(self, data: bytes | str) -> GaloisKeys: ... + + +class CoeffModulus: + @staticmethod + def MaxBitCount(poly_modulus_degree: int, sec_level: sec_level_type = sec_level_type.tc128) -> int: ... + @staticmethod + def BFVDefault(poly_modulus_degree: int, sec_level: sec_level_type = sec_level_type.tc128) -> Sequence[Modulus]: ... + @overload + @staticmethod + def Create(poly_modulus_degree: int, bit_sizes: Sequence[int]) -> Sequence[Modulus]: ... + @overload + @staticmethod + def Create(poly_modulus_degree: int, plain_modulus: Modulus, bit_sizes: Sequence[int]) -> Sequence[Modulus]: ... + + +class PlainModulus: + @overload + @staticmethod + def Batching(poly_modulus_degree: int, bit_size: int) -> Modulus: ... + @overload + @staticmethod + def Batching(poly_modulus_degree: int, bit_sizes: Sequence[int]) -> Sequence[Modulus]: ... + + +class Plaintext: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, coeff_count: int) -> None: ... + @overload + def __init__(self, coeff_count: int, capacity: int) -> None: ... + @overload + def __init__(self, hex_poly: str) -> None: ... + @overload + def __init__(self, other: Plaintext) -> None: ... + @overload + def set_zero(self) -> None: ... + @overload + def set_zero(self, start_coeff: int) -> None: ... + @overload + def set_zero(self, start_coeff: int, length: int) -> None: ... + def is_zero(self) -> bool: ... + def capacity(self) -> int: ... + def coeff_count(self) -> int: ... + def significant_coeff_count(self) -> int: ... + def nonzero_coeff_count(self) -> int: ... + def to_string(self) -> str: ... + def is_ntt_form(self) -> bool: ... + def parms_id(self) -> ParmsId: ... + @overload + def scale(self) -> float: ... + @overload + def scale(self, value: float) -> None: ... + @overload + def save(self, path: str) -> None: ... + @overload + def save(self, path: str, compr_mode: compr_mode_type) -> None: ... + def load(self, context: SEALContext, path: str) -> None: ... + def load_bytes(self, context: SEALContext, data: bytes) -> None: ... + def save_size(self, compr_mode: compr_mode_type = ...) -> int: ... + def to_bytes(self, compr_mode: compr_mode_type = ...) -> bytes: ... + + +class Ciphertext: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, context: SEALContext) -> None: ... + @overload + def __init__(self, context: SEALContext, parms_id: ParmsId) -> None: ... + @overload + def __init__(self, context: SEALContext, parms_id: ParmsId, size_capacity: int) -> None: ... + @overload + def __init__(self, other: Ciphertext) -> None: ... + def coeff_modulus_size(self) -> int: ... + def poly_modulus_degree(self) -> int: ... + def size(self) -> int: ... + def size_capacity(self) -> int: ... + def is_transparent(self) -> bool: ... + def is_ntt_form(self) -> bool: ... + def parms_id(self) -> ParmsId: ... + @overload + def scale(self) -> float: ... + @overload + def scale(self, value: float) -> None: ... + @overload + def save(self, path: str) -> None: ... + @overload + def save(self, path: str, compr_mode: compr_mode_type) -> None: ... + def load(self, context: SEALContext, path: str) -> None: ... + def load_bytes(self, context: SEALContext, data: bytes) -> None: ... + def save_size(self, compr_mode: compr_mode_type = ...) -> int: ... + def to_string(self, compr_mode: compr_mode_type = ...) -> bytes: ... + + +class SecretKey: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, other: SecretKey) -> None: ... + def parms_id(self) -> ParmsId: ... + def save(self, path: str) -> None: ... + def load(self, context: SEALContext, path: str) -> None: ... + def to_string(self) -> bytes: ... + + +class PublicKey: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, other: PublicKey) -> None: ... + def parms_id(self) -> ParmsId: ... + def save(self, path: str) -> None: ... + def load(self, context: SEALContext, path: str) -> None: ... + def to_string(self) -> bytes: ... + + +class KSwitchKeys: + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, other: KSwitchKeys) -> None: ... + def size(self) -> int: ... + def parms_id(self) -> ParmsId: ... + def save(self, path: str) -> None: ... + def load(self, context: SEALContext, path: str) -> None: ... + + +class RelinKeys(KSwitchKeys): + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, other: KSwitchKeys) -> None: ... + @staticmethod + def get_index(key_power: int) -> int: ... + def has_key(self, key_power: int) -> bool: ... + def to_string(self) -> bytes: ... + + +class GaloisKeys(KSwitchKeys): + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, other: KSwitchKeys) -> None: ... + @staticmethod + def get_index(galois_elt: int) -> int: ... + def has_key(self, galois_elt: int) -> bool: ... + def to_string(self) -> bytes: ... + + +class KeyGenerator: + @overload + def __init__(self, context: SEALContext) -> None: ... + @overload + def __init__(self, context: SEALContext, secret_key: SecretKey) -> None: ... + def secret_key(self) -> SecretKey: ... + @overload + def create_public_key(self) -> PublicKey: ... + @overload + def create_public_key(self, destination: PublicKey) -> None: ... + @overload + def create_relin_keys(self) -> RelinKeys: ... + @overload + def create_relin_keys(self, destination: RelinKeys) -> None: ... + @overload + def create_galois_keys(self) -> GaloisKeys: ... + @overload + def create_galois_keys(self, destination: GaloisKeys) -> None: ... + @overload + def create_galois_keys(self, galois_elts: Sequence[int], destination: GaloisKeys) -> None: ... + + +class Encryptor: + @overload + def __init__(self, context: SEALContext, public_key: PublicKey) -> None: ... + @overload + def __init__(self, context: SEALContext, secret_key: SecretKey) -> None: ... + @overload + def __init__(self, context: SEALContext, public_key: PublicKey, secret_key: SecretKey) -> None: ... + def set_public_key(self, public_key: PublicKey) -> None: ... + def set_secret_key(self, secret_key: SecretKey) -> None: ... + @overload + def encrypt_zero(self) -> Ciphertext: ... + @overload + def encrypt_zero(self, destination: Ciphertext) -> None: ... + @overload + def encrypt_zero(self, parms_id: ParmsId) -> Ciphertext: ... + @overload + def encrypt_zero(self, parms_id: ParmsId, destination: Ciphertext) -> None: ... + @overload + def encrypt(self, plain: Plaintext) -> Ciphertext: ... + @overload + def encrypt(self, plain: Plaintext, destination: Ciphertext) -> None: ... + @overload + def encrypt_symmetric(self, plain: Plaintext) -> Ciphertext: ... + @overload + def encrypt_symmetric(self, plain: Plaintext, destination: Ciphertext) -> None: ... + + +class Evaluator: + def __init__(self, context: SEALContext) -> None: ... + def negate_inplace(self, encrypted: Ciphertext) -> None: ... + def negate(self, encrypted1: Ciphertext) -> Ciphertext: ... + def add_inplace(self, encrypted1: Ciphertext, encrypted2: Ciphertext) -> None: ... + def add(self, encrypted1: Ciphertext, encrypted2: Ciphertext) -> Ciphertext: ... + def add_many(self, encrypteds: Sequence[Ciphertext]) -> Ciphertext: ... + def sub_inplace(self, encrypted1: Ciphertext, encrypted2: Ciphertext) -> None: ... + def sub(self, encrypted1: Ciphertext, encrypted2: Ciphertext) -> Ciphertext: ... + def multiply_inplace(self, encrypted1: Ciphertext, encrypted2: Ciphertext) -> None: ... + def multiply(self, encrypted1: Ciphertext, encrypted2: Ciphertext) -> Ciphertext: ... + def square_inplace(self, encrypted1: Ciphertext) -> None: ... + def square(self, encrypted1: Ciphertext) -> Ciphertext: ... + def relinearize_inplace(self, encrypted1: Ciphertext, relin_keys: RelinKeys) -> None: ... + def relinearize(self, encrypted1: Ciphertext, relin_keys: RelinKeys) -> Ciphertext: ... + @overload + def mod_switch_to_next(self, encrypted: Ciphertext) -> Ciphertext: ... + @overload + def mod_switch_to_next(self, plain: Plaintext) -> Plaintext: ... + @overload + def mod_switch_to_next_inplace(self, encrypted: Ciphertext) -> None: ... + @overload + def mod_switch_to_next_inplace(self, plain: Plaintext) -> None: ... + @overload + def mod_switch_to_inplace(self, encrypted: Ciphertext, parms_id: ParmsId) -> None: ... + @overload + def mod_switch_to_inplace(self, plain: Plaintext, parms_id: ParmsId) -> None: ... + @overload + def mod_switch_to(self, encrypted: Ciphertext, parms_id: ParmsId) -> Ciphertext: ... + @overload + def mod_switch_to(self, plain: Plaintext, parms_id: ParmsId) -> Plaintext: ... + def rescale_to_next(self, encrypted: Ciphertext) -> Ciphertext: ... + def rescale_to_next_inplace(self, encrypted: Ciphertext) -> None: ... + def rescale_to_inplace(self, encrypted: Ciphertext, parms_id: ParmsId) -> None: ... + def rescale_to(self, encrypted: Ciphertext, parms_id: ParmsId) -> Ciphertext: ... + def multiply_many(self, encrypteds: Sequence[Ciphertext], relin_keys: RelinKeys) -> Ciphertext: ... + def exponentiate_inplace(self, encrypted: Ciphertext, exponent: int, relin_keys: RelinKeys) -> None: ... + def exponentiate(self, encrypted: Ciphertext, exponent: int, relin_keys: RelinKeys) -> Ciphertext: ... + def add_plain_inplace(self, encrypted: Ciphertext, plain: Plaintext) -> None: ... + def add_plain(self, encrypted: Ciphertext, plain: Plaintext) -> Ciphertext: ... + def sub_plain_inplace(self, encrypted: Ciphertext, plain: Plaintext) -> None: ... + def sub_plain(self, encrypted: Ciphertext, plain: Plaintext) -> Ciphertext: ... + def multiply_plain_inplace(self, encrypted: Ciphertext, plain: Plaintext) -> None: ... + def multiply_plain(self, encrypted: Ciphertext, plain: Plaintext) -> Ciphertext: ... + @overload + def transform_to_ntt_inplace(self, plain: Plaintext, parms_id: ParmsId) -> None: ... + @overload + def transform_to_ntt_inplace(self, encrypted: Ciphertext) -> None: ... + @overload + def transform_to_ntt(self, plain: Plaintext, parms_id: ParmsId) -> Plaintext: ... + @overload + def transform_to_ntt(self, encrypted: Ciphertext) -> Ciphertext: ... + def transform_from_ntt_inplace(self, encrypted: Ciphertext) -> None: ... + def transform_from_ntt(self, encrypted_ntt: Ciphertext) -> Ciphertext: ... + def apply_galois_inplace(self, encrypted: Ciphertext, galois_elt: int, galois_keys: GaloisKeys) -> None: ... + def apply_galois(self, encrypted: Ciphertext, galois_elt: int, galois_keys: GaloisKeys) -> Ciphertext: ... + def rotate_rows_inplace(self, encrypted: Ciphertext, steps: int, galois_keys: GaloisKeys) -> None: ... + def rotate_rows(self, encrypted: Ciphertext, steps: int, galois_keys: GaloisKeys) -> Ciphertext: ... + def rotate_columns_inplace(self, encrypted: Ciphertext, galois_keys: GaloisKeys) -> None: ... + def rotate_columns(self, encrypted: Ciphertext, galois_keys: GaloisKeys) -> Ciphertext: ... + def rotate_vector_inplace(self, encrypted: Ciphertext, steps: int, galois_keys: GaloisKeys) -> None: ... + def rotate_vector(self, encrypted: Ciphertext, steps: int, galois_keys: GaloisKeys) -> Ciphertext: ... + def complex_conjugate_inplace(self, encrypted: Ciphertext, galois_keys: GaloisKeys) -> None: ... + def complex_conjugate(self, encrypted: Ciphertext, galois_keys: GaloisKeys) -> Ciphertext: ... + + +class CKKSEncoder: + def __init__(self, context: SEALContext) -> None: ... + def slot_count(self) -> int: ... + @overload + def encode(self, values: FloatLikeArray, scale: float) -> Plaintext: ... + @overload + def encode(self, values: FloatLikeArray, scale: float, destination: Plaintext) -> None: ... + @overload + def encode(self, value: float, scale: float) -> Plaintext: ... + @overload + def encode(self, value: float, scale: float, destination: Plaintext) -> None: ... + @overload + def encode(self, value: int) -> Plaintext: ... + @overload + def encode(self, value: int, destination: Plaintext) -> None: ... + @overload + def encode_complex(self, values: ComplexLikeArray, scale: float) -> Plaintext: ... + @overload + def encode_complex(self, values: ComplexLikeArray, scale: float, destination: Plaintext) -> None: ... + @overload + def encode_complex(self, value: complex, scale: float) -> Plaintext: ... + @overload + def encode_complex(self, value: complex, scale: float, destination: Plaintext) -> None: ... + def decode(self, plain: Plaintext) -> NDArray[np.float64]: ... + def decode_complex(self, plain: Plaintext) -> NDArray[np.complex128]: ... + + +class Decryptor: + def __init__(self, context: SEALContext, secret_key: SecretKey) -> None: ... + @overload + def decrypt(self, encrypted: Ciphertext, destination: Plaintext) -> None: ... + @overload + def decrypt(self, encrypted: Ciphertext) -> Plaintext: ... + def invariant_noise_budget(self, encrypted: Ciphertext) -> int: ... + + +class BatchEncoder: + def __init__(self, context: SEALContext) -> None: ... + def slot_count(self) -> int: ... + @overload + def encode(self, values: Sequence[int], destination: Plaintext) -> None: ... + @overload + def encode(self, values: IntLikeArray) -> Plaintext: ... + def decode(self, plain: Plaintext) -> NDArray[np.int64]: ... + def decode_uint64(self, plain: Plaintext) -> NDArray[np.uint64]: ... diff --git a/seal_python.egg-info/PKG-INFO b/seal_python.egg-info/PKG-INFO new file mode 100644 index 0000000..cd7614e --- /dev/null +++ b/seal_python.egg-info/PKG-INFO @@ -0,0 +1,231 @@ +Metadata-Version: 2.4 +Name: seal-python +Version: 4.1.2 +Summary: Python wrapper for the Microsoft SEAL +Home-page: https://github.com/Huelse/SEAL-Python +Author: Huelse +Author-email: topmaxz@protonmail.com +License: MIT +Project-URL: Repository, https://github.com/Huelse/SEAL-Python +Project-URL: Issues, https://github.com/Huelse/SEAL-Python/issues +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: C++ +Classifier: Topic :: Security :: Cryptography +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +## Microsoft SEAL For Python + +Microsoft [**SEAL**](https://github.com/microsoft/SEAL) is an easy-to-use open-source ([MIT licensed](https://github.com/microsoft/SEAL/blob/master/LICENSE)) homomorphic encryption library developed by the Cryptography Research group at Microsoft. + +[**pybind11**](https://github.com/pybind/pybind11) is a lightweight header-only library that exposes C++ types in Python and vice versa, mainly to create Python bindings of existing C++ code. + +This is a python binding for the Microsoft SEAL library. + + + +## Contents + +* [Build](#build) +* [Typing](#typing) +* [Note](#note) + * [Serialize](#serialize) + * [Other](#other) +* [FAQ](#faq) +* [Release](#release) + + + +## Build + +* ### Linux + + Recommend: Clang++ (>= 10.0) or GNU G++ (>= 9.4), CMake (>= 3.16) + + ```shell + # Optional + sudo apt-get install git build-essential cmake python3 python3-dev python3-pip + + # Get the repository or download from the releases + git clone https://github.com/Huelse/SEAL-Python.git + cd SEAL-Python + + # Install dependencies + pip3 install numpy pybind11 + + # Init the SEAL and pybind11 + git submodule update --init --recursive + # Get the newest repositories (dev only) + # git submodule update --remote + + # Build the SEAL lib without the msgsl zlib and zstandard compression + cd SEAL + cmake -S . -B build -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF -DSEAL_USE_ZSTD=OFF + cmake --build build + cd .. + + # Run the setup.py, the dynamic library will be generated in the current directory + python3 setup.py build_ext -i + + # Test + cp seal.*.so examples + cd examples + python3 4_bgv_basics.py + ``` + + Build examples: `-DSEAL_BUILD_EXAMPLES=ON` + + [More cmake options](https://github.com/microsoft/SEAL#basic-cmake-options) + + +* ### Windows + + Visual Studio 2019 or newer is required. x64 support only! And use the **x64 Native Tools Command Prompt for VS** command prompt to configure and build the Microsoft SEAL library. It's usually can be found in your Start Menu. + + ```shell + # Run in "x64 Native Tools Command Prompt for VS" command prompt + cmake -S . -B build -G Ninja -DSEAL_USE_MSGSL=OFF -DSEAL_USE_ZLIB=OFF + cmake --build build + + # Build + pip install numpy pybind11 + python setup.py build_ext -i + + # Test + cp seal.*.pyd examples + cd examples + python 4_bgv_basics.py + ``` + + Microsoft SEAL official [docs](https://github.com/microsoft/SEAL#building-microsoft-seal-manually). + + +* ### Docker + + requires: [Docker](https://www.docker.com/) + + To build source code into a docker image (from this directory): + ```shell + docker build -t huelse/seal -f Dockerfile . + ``` + + To use the image by running it as an interactive container: + ```shell + docker run -it huelse/seal + ``` + + + +## Note + +* ### Serialize + + See more in `examples/7_serialization.py`, here is a simple example: + + ```python + cipher.save('cipher') + load_cipher = Ciphertext() + load_cipher.load(context, 'cipher') # work if the context is valid. + ``` + + Supported classes: `EncryptionParameters, Ciphertext, Plaintext, SecretKey, PublicKey, RelinKeys, GaloisKeys` + + +* ### Other + + There are a lot of changes in the latest SEAL lib, we try to make the API in python can be used easier, but it may remain some problems unknown, if any problems or bugs, report [issues](https://github.com/Huelse/SEAL-Python/issues). + + Email: [topmaxz@protonmail.com](mailto:topmaxz@protonmail.com?subject=Github-SEAL-Python-Issues) + + + +## FAQ + +1. ImportError: undefined symbol + + Build a shared SEAL library `cmake . -DBUILD_SHARED_LIBS=ON`, and get the `libseal.so`, + + then change the path in `setup.py`, and rebuild. + + +2. ImportError: libseal.so... cannot find + + a. `sudo ln -s /path/to/libseal.so /usr/lib` + + b. add `/usr/local/lib` or the `SEAL/native/lib` to `/etc/ld.so.conf` and refresh it `sudo ldconfig` + + c. build in cmake. + + +3. BuildError: + + 1. C++17 at least + + 2. x86_64 is required, which `x86_32` is not supported + + +4. ModuleNotFoundError: No module named 'seal' + + The `.so` or `.pyd` file must be in the current directory, or you have `install` it already. + + +5. Windows Error LNK2001, RuntimeLibrary and MT_StaticRelease mismatch + + Only `x64` is supported, Choose `x64 Native Tools Command Prompt for VS`. + + +6. Warning about building the dynamic library with static library in MacOS, etc. + + 1. Build a shared SEAL library by adding a CMake option `-DBUILD_SHARED_LIBS=ON` + + 2. Edit `extra_objects` in setup.py to `*.dylib` or else. + + +## Typing + +This project now ships `seal.pyi` and `py.typed` for Python type checking (PEP 561). + +After build/install, editors such as Pylance/Pyright/mypy can use these hints for autocomplete and static analysis. + + +## Release + +Use `RELEASE.md` for the full PyPI release checklist. + +Quick commands: + +```bash +python3 setup.py sdist bdist_wheel +python3 -m twine check dist/* +python3 -m twine upload dist/* +``` + + + +## Contributing + +* Professor: [Dr. Chen](https://zhigang-chen.github.io/) + +* [Contributors](https://github.com/Huelse/SEAL-Python/graphs/contributors) diff --git a/seal_python.egg-info/SOURCES.txt b/seal_python.egg-info/SOURCES.txt new file mode 100644 index 0000000..cf0febc --- /dev/null +++ b/seal_python.egg-info/SOURCES.txt @@ -0,0 +1,13 @@ +LICENSE +MANIFEST.in +README.md +py.typed +pyproject.toml +seal.pyi +setup.py +seal_python.egg-info/PKG-INFO +seal_python.egg-info/SOURCES.txt +seal_python.egg-info/dependency_links.txt +seal_python.egg-info/not-zip-safe +seal_python.egg-info/top_level.txt +src/wrapper.cpp \ No newline at end of file diff --git a/seal_python.egg-info/dependency_links.txt b/seal_python.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/seal_python.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/seal_python.egg-info/not-zip-safe b/seal_python.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/seal_python.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/seal_python.egg-info/top_level.txt b/seal_python.egg-info/top_level.txt new file mode 100644 index 0000000..b93463e --- /dev/null +++ b/seal_python.egg-info/top_level.txt @@ -0,0 +1 @@ +seal diff --git a/setup.py b/setup.py index 0147f31..0d2862b 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,57 @@ import os import platform from glob import glob +from pathlib import Path +from shutil import copy2 +import sys from setuptools import setup from distutils.sysconfig import get_python_inc from pybind11.setup_helpers import Pybind11Extension, build_ext -__version__ = "4.0.0" +__version__ = "4.1.2" +BASE_DIR = Path(__file__).resolve().parent include_dirs = [get_python_inc(), 'pybind11/include', 'SEAL/native/src', 'SEAL/build/native/src'] -extra_objects = sorted(glob('SEAL/build/lib/*.lib') if platform.system() == "Windows" else glob('SEAL/build/lib/*.a')) +if platform.system() == "Windows": + extra_objects = sorted( + glob('SEAL/build/lib/*.lib') + + glob('SEAL/build/lib/Release/*.lib') + + glob('SEAL/build/**/libseal*.lib', recursive=True) + ) +else: + extra_objects = sorted(glob('SEAL/build/lib/*.a')) cpp_args = ['/std:c++latest'] if platform.system() == "Windows" else ['-std=c++17'] if len(extra_objects) < 1 or not os.path.exists(extra_objects[0]): print('Not found the seal lib file, check the `SEAL/build/lib`') - exit(0) + sys.exit(1) + + +class build_ext_with_typing(build_ext): + """Copy PEP 561 typing files next to the compiled extension.""" + typing_files = ("seal.pyi", "py.typed") + + def run(self): + super().run() + self._copy_typing_files() + + def _copy_typing_files(self): + output_dirs = {Path(self.build_lib)} + if self.inplace: + output_dirs.add(BASE_DIR) + + for target_dir in output_dirs: + target_dir.mkdir(parents=True, exist_ok=True) + for filename in self.typing_files: + source = BASE_DIR / filename + if source.exists(): + destination = target_dir / filename + if source.resolve() == destination.resolve(): + continue + copy2(source, destination) + ext_modules = [ Pybind11Extension( @@ -29,16 +65,36 @@ ] setup( - name="seal", + name="seal-python", version=__version__, author="Huelse", author_email="topmaxz@protonmail.com", url="https://github.com/Huelse/SEAL-Python", description="Python wrapper for the Microsoft SEAL", - long_description="", + long_description=(BASE_DIR / "README.md").read_text(encoding="utf-8"), + long_description_content_type="text/markdown", ext_modules=ext_modules, - cmdclass={"build_ext": build_ext}, + cmdclass={"build_ext": build_ext_with_typing}, zip_safe=False, license='MIT', - python_requires=">=3.6", + python_requires=">=3.8", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: C++", + "Topic :: Security :: Cryptography", + ], + project_urls={ + "Repository": "https://github.com/Huelse/SEAL-Python", + "Issues": "https://github.com/Huelse/SEAL-Python/issues", + }, )