Source code for eclib.gsw

#! /usr/bin/env python3

"""GSW encryption scheme.

This module implements the GSW encryption scheme, which is a fully homomorphic
encryption scheme based on the Learning with Errors (LWE) problem. It provides
functionalities for generating public and secret keys, encryption, decryption, and
homomorphic operations (addition, multiplication, and integer multiplication). It also
includes functions for encoding and decoding floating-point data into and from
plaintexts.

Classes
-------
- PublicParameters

Functions
---------
- keygen
- encrypt
- decrypt
- add
- elementwise_add
- mult
- elementwise_mult
- int_mult
- elementwise_int_mult
- encode
- decode
- enc
- dec
"""

from dataclasses import dataclass
from math import ceil, floor, log2
from typing import Optional

import numpy as np
from numpy.typing import ArrayLike, NDArray

import eclib.randutils as ru
from eclib import regev
from eclib.regev import PublicKey, SecretKey


[docs] @dataclass(slots=True) class PublicParameters: """ Represents public parameters of the GSW encryption scheme. Attributes ---------- n : int Dimension of a lattice, which is equal to the dimension of secret key. q : int Modulus of plaintext and ciphertext spaces. sigma : float Standard deviation of the discrete Gaussian distribution with mean zero used as an error distribution. m : int Subdimension of the lattice. l : int Bit length of the modulus. N : int `N = (n + 1) * l`. """ n: int q: int sigma: float m: int l: int N: int
[docs] def __init__(self, n: int, q: int, sigma: float, m: Optional[int] = None): """ Initializes a new PublicParameters object. Parameters ---------- n : int Dimension of a lattice, which is equal to the dimension of secret key. q : int Modulus of plaintext and ciphertext spaces. sigma : float Standard deviation of the discrete Gaussian distribution with mean zero used as an error distribution. m : int, optional Subdimension of the lattice. Returns ------- None Note ---- If `m` is not provided, it is set to `2 * n * ceil(log2(q))`. """ self.n = n self.q = q self.sigma = sigma self.l = ceil(log2(q)) self.N = (n + 1) * self.l if m is None: self.m = 2 * n * ceil(log2(q)) else: self.m = m
[docs] def keygen(n: int, q: int, sigma: float, m: Optional[int] = None): """ Generates public parameters, a public key, and a secret key. Parameters ---------- n : int Dimension of a lattice, which is equal to the dimension of secret key. q : int Modulus of plaintext and ciphertext spaces. sigma : float Standard deviation of the discrete Gaussian distribution with mean zero used as an error distribution. m : int, optional Subdimension of the lattice. Returns ------- params : eclib.gsw.PublicParameters Cryptosystem parameters. pk : eclib.regev.PublicKey Public key used for encryption. sk : eclib.regev.SecretKey Secret key used for decryption. Note ---- If `m` is not provided, it is set to `2 * n * ceil(log2(q))`. See Also -------- eclib.gsw.PublicParameters eclib.regev.PublicKey eclib.regev.SecretKey """ params = PublicParameters(n, q, sigma, m) lwe_params = regev.PublicParameters(n, 0, q, sigma, m) sk = SecretKey(lwe_params) pk = PublicKey(lwe_params, sk) return params, pk, sk
[docs] def encrypt( params: PublicParameters, pk: PublicKey, m: ArrayLike ) -> NDArray[np.object_]: """ Encrypts a scalar, vector, or matrix plaintext `m` using a public key `pk`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. pk : eclib.regev.PublicKey Public key used for encryption. m : array_like Plaintext to be encrypted. Returns ------- numpy.ndarray Ciphertext of the plaintext. Raises ------ ValueError If the plaintext is not a scalar, vector, or matrix. See Also -------- eclib.gsw.decrypt eclib.gsw.enc """ m = np.asarray(m, dtype=object) match m.ndim: case 0: return _encrypt(params, pk, m.item()) case 1: return np.array( [_encrypt(params, pk, m[i]) for i in range(m.shape[0])], dtype=object, ) case 2: return np.array( [ [_encrypt(params, pk, m[i][j]) for j in range(m.shape[1])] for i in range(m.shape[0]) ], dtype=object, ) case _: raise ValueError
[docs] def decrypt( params: PublicParameters, sk: SecretKey, c: NDArray[np.object_] ) -> ArrayLike: """ Decrypts a scalar, vector, or matrix ciphertext `c` using a secret key `sk`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. sk : eclib.regev.SecretKey Secret key used for decryption. c : numpy.ndarray Ciphertext to be decrypted. Returns ------- array_like Decrypted plaintext. Raises ------ ValueError If the ciphertext is not a scalar, vector, or matrix. See Also -------- eclib.gsw.encrypt eclib.gsw.dec """ c = np.asarray(c, dtype=object) match c.ndim - 2: case 0: return _decrypt(params, sk, c) case 1: return np.array( [_decrypt(params, sk, c[i]) for i in range(c.shape[0])], dtype=object, ) case 2: return np.array( [ [_decrypt(params, sk, c[i][j]) for j in range(c.shape[1])] for i in range(c.shape[0]) ], dtype=object, ) case _: raise ValueError
[docs] def add( params: PublicParameters, c1: NDArray[np.object_], c2: NDArray[np.object_], ) -> NDArray[np.object_]: """ Computes a ciphertext of the addition of two scalar, vector, or matrix plaintexts corresponding to ciphertexts `c1` and `c2`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. c1 : numpy.ndarray Ciphertext of the first plaintext. c2 : numpy.ndarray Ciphertext of the second plaintext. Returns ------- numpy.ndarray Ciphertext of the addition of the plaintexts. Raises ------ ValueError If the ciphertexts are not the following types of appropriate sizes: scalar-scalar, vector-vector, or matrix-matrix. See Also -------- eclib.gsw.elementwise_add """ c1 = np.asarray(c1, dtype=object) c2 = np.asarray(c2, dtype=object) if c1.shape == c2.shape: match c1.ndim - 2: case 0: return _add(params, c1, c2) case 1: return np.array( [_add(params, c1[i], c2[i]) for i in range(c1.shape[0])], dtype=object, ) case 2: return np.array( [ [_add(params, c1[i][j], c2[i][j]) for j in range(c1.shape[1])] for i in range(c1.shape[0]) ], dtype=object, ) case _: raise ValueError else: raise ValueError
[docs] def elementwise_add( params: PublicParameters, c1: NDArray[np.object_], c2: NDArray[np.object_], ) -> NDArray[np.object_]: """ Computes a ciphertext of the elementwise addition of two scalar, vector, or matrix plaintexts corresponding to ciphertexts `c1` and `c2`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. c1 : numpy.ndarray Ciphertext of the first plaintext. c2 : numpy.ndarray Ciphertext of the second plaintext. Returns ------- numpy.ndarray Ciphertext of the elementwise addition of the plaintexts. Raises ------ ValueError If the ciphertexts are not the following types of appropriate sizes: scalar-scalar, vector-vector, matrix-vector, or matrix-matrix. See Also -------- eclib.gsw.add """ c1 = np.asarray(c1, dtype=object) c2 = np.asarray(c2, dtype=object) if c1.shape == c2.shape: return add(params, c1, c2) elif c1.ndim - 2 == 2 and c1.shape[1] == c2.shape[0]: return np.array( [ [_add(params, c1[i][j], c2[j]) for j in range(c1.shape[1])] for i in range(c1.shape[0]) ], dtype=object, ) else: raise ValueError
[docs] def mult( params: PublicParameters, c1: NDArray[np.object_], c2: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the product of two scalar, vector, or matrix plaintexts corresponding to ciphertexts `c1` and `c2`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. c1 : numpy.ndarray Ciphertext of the first plaintext. c2 : numpy.ndarray Ciphertext of the second plaintext. Returns ------- numpy.ndarray Ciphertext of the product of the plaintexts. Raises ------ ValueError If the ciphertexts are not the following types of appropriate sizes: scalar-scalar, scalar-vector, scalar-matrix, vector-vector, matrix-vector, or matrix-matrix. See Also -------- eclib.gsw.elementwise_mult """ c1 = np.asarray(c1, dtype=object) c2 = np.asarray(c2, dtype=object) match c1.ndim - 2: case 0 if c2.ndim - 2 == 0: return _mult(params, c1, c2) case 0 if c2.ndim - 2 == 1: return np.array( [_mult(params, c1, c2[i]) for i in range(c2.shape[0])], dtype=object, ) case 0 if c2.ndim - 2 == 2: return np.array( [ [_mult(params, c1, c2[i][j]) for j in range(c2.shape[1])] for i in range(c2.shape[0]) ], dtype=object, ) case 1 if c2.ndim - 2 == 1 and c1.shape[0] == c2.shape[0]: c_s = np.zeros([params.n + 1, params.N], dtype=object) for i in range(c1.shape[0]): c_s = _add(params, c_s, _mult(params, c1[i], c2[i])) return c_s case 2 if c2.ndim - 2 == 1 and c1.shape[1] == c2.shape[0]: c_v = np.zeros([c1.shape[0], params.n + 1, params.N], dtype=object) for i in range(c1.shape[0]): for j in range(c1.shape[1]): c_v[i] = _add(params, c_v[i], _mult(params, c1[i][j], c2[j])) return c_v case 2 if c2.ndim - 2 == 2 and c1.shape[1] == c2.shape[0]: c_m = np.zeros( [c1.shape[0], c2.shape[1], params.n + 1, params.N], dtype=object ) for i in range(c1.shape[0]): for j in range(c2.shape[1]): for k in range(c1.shape[1]): c_m[i][j] = _add( params, c_m[i][j], _mult(params, c1[i][k], c2[k][j]) ) return c_m case _: raise ValueError
[docs] def elementwise_mult( params: PublicParameters, c1: NDArray[np.object_], c2: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the elementwise product of two scalar, vector, or matrix plaintexts corresponding to ciphertexts `c1` and `c2`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. c1 : numpy.ndarray Ciphertext of the first plaintext. c2 : numpy.ndarray Ciphertext of the second plaintext. Returns ------- numpy.ndarray Ciphertext of the elementwise product of the plaintexts. Raises ------ ValueError If the ciphertexts are not the following types of appropriate sizes: scalar-scalar, scalar-vector, scalar-matrix, vector-vector, matrix-vector, or matrix-matrix. See Also -------- eclib.gsw.mult """ c1 = np.asarray(c1, dtype=object) c2 = np.asarray(c2, dtype=object) match c1.ndim - 2: case 0: return mult(params, c1, c2) case 1 if c2.ndim - 2 == 1 and c1.shape[0] == c2.shape[0]: return np.array( [_mult(params, c1[i], c2[i]) for i in range(c1.shape[0])], dtype=object, ) case 2 if c2.ndim - 2 == 1 and c1.shape[1] == c2.shape[0]: return np.array( [ [_mult(params, c1[i][j], c2[j]) for j in range(c1.shape[1])] for i in range(c1.shape[0]) ], dtype=object, ) case 2 if c2.ndim - 2 == 2 and c1.shape[1] == c2.shape[0]: return np.array( [ [_mult(params, c1[i][j], c2[i][j]) for j in range(c1.shape[1])] for i in range(c1.shape[0]) ], dtype=object, ) case _: raise ValueError
[docs] def int_mult( params: PublicParameters, m: ArrayLike, c: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the product of a scalar, vector, or matrix plaintext `m` and another scalar, vector, or matrix plaintext corresponding to a ciphertext `c`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. m : array_like Plaintext to be multiplied. c : numpy.ndarray Ciphertext of a plaintext to be multiplied. Returns ------- numpy.ndarray Ciphertext of the product of the plaintexts. Raises ------ ValueError If the plaintext and ciphertext are not the following types of appropriate sizes: scalar-scalar, scalar-vector, scalar-matrix, vector-vector, matrix-vector, or matrix-matrix. See Also -------- eclib.gsw.elementwise_int_mult """ m = np.asarray(m, dtype=object) c = np.asarray(c, dtype=object) match m.ndim: case 0 if c.ndim - 2 == 0: return _int_mult(params, m.item(), c) case 0 if c.ndim - 2 == 1: return np.array( [_int_mult(params, m.item(), c[i]) for i in range(c.shape[0])], dtype=object, ) case 0 if c.ndim - 2 == 2: return np.array( [ [_int_mult(params, m.item(), c[i][j]) for j in range(c.shape[1])] for i in range(c.shape[0]) ], dtype=object, ) case 1 if c.ndim - 2 == 1 and m.shape[0] == c.shape[0]: c_s = np.zeros([params.n + 1, params.N], dtype=object) for i in range(m.shape[0]): c_s = _add(params, c_s, _int_mult(params, m[i], c[i])) return c_s case 2 if c.ndim - 2 == 1 and m.shape[1] == c.shape[0]: c_v = np.zeros([m.shape[0], params.n + 1, params.N], dtype=object) for i in range(m.shape[0]): for j in range(m.shape[1]): c_v[i] = _add(params, c_v[i], _int_mult(params, m[i][j], c[j])) return c_v case 2 if c.ndim - 2 == 2 and m.shape[1] == c.shape[0]: c_m = np.zeros( [m.shape[0], c.shape[1], params.n + 1, params.N], dtype=object ) for i in range(m.shape[0]): for j in range(c.shape[1]): for k in range(m.shape[1]): c_m[i][j] = _add( params, c_m[i][j], _int_mult(params, m[i][k], c[k][j]) ) return c_m case _: raise ValueError
[docs] def elementwise_int_mult( params: PublicParameters, m: ArrayLike, c: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the elementwise product of a scalar, vector, or matrix plaintext `m` and another scalar, vector, or matrix plaintext corresponding to a ciphertext `c`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. m : array_like Plaintext to be multiplied. c : numpy.ndarray Ciphertext of a plaintext to be multiplied. Returns ------- numpy.ndarray Ciphertext of the elementwise product of the plaintexts. Raises ------ ValueError If the plaintext and ciphertext are not the following types of appropriate sizes: scalar-scalar, scalar-vector, scalar-matrix, vector-vector, matrix-vector, or matrix-matrix. See Also -------- eclib.gsw.int_mult """ m = np.asarray(m, dtype=object) c = np.asarray(c, dtype=object) match m.ndim: case 0: return int_mult(params, m.item(), c) case 1 if c.ndim - 2 == 1 and m.shape[0] == c.shape[0]: return np.array( [_int_mult(params, m[i], c[i]) for i in range(m.shape[0])], dtype=object, ) case 2 if c.ndim - 2 == 1 and m.shape[1] == c.shape[0]: return np.array( [ [_int_mult(params, m[i][j], c[j]) for j in range(m.shape[1])] for i in range(c.shape[0]) ], dtype=object, ) case 2 if c.ndim - 2 == 2 and m.shape[1] == c.shape[0]: return np.array( [ [_int_mult(params, m[i][j], c[i][j]) for j in range(m.shape[1])] for i in range(m.shape[0]) ], dtype=object, ) case _: raise ValueError
[docs] def encode(params: PublicParameters, x: ArrayLike, scale: float) -> ArrayLike: """ Encodes a scalar, vector, or matrix floating-point data `x` into a plaintext. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. x : array_like Floating-point data to be encoded. scale : float Scaling factor. Returns ------- array_like Encoded plaintext. See Also -------- eclib.gsw.decode eclib.gsw.enc """ f = np.frompyfunc(_encode, 3, 1) return f(params, x, scale)
[docs] def decode(params: PublicParameters, m: ArrayLike, scale: float) -> ArrayLike: """ Decodes a scalar, vector, or matrix plaintext `m` into a floating-point data. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. m : array_like Plaintext to be decoded. scale : float Scaling factor. Returns ------- array_like Decoded floating-point data. See Also -------- eclib.gsw.encode eclib.gsw.dec """ f = np.frompyfunc(_decode, 3, 1) return f(params, m, scale)
[docs] def enc( params: PublicParameters, pk: PublicKey, x: ArrayLike, scale: float ) -> NDArray[np.object_]: """ Encodes and encrypts a scalar, vector, or matrix floating-point data `x` using a public key `pk`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. pk : eclib.regev.PublicKey Public key used for encryption. x : array_like Floating-point data to be encoded and encrypted. scale : float Scaling factor. Returns ------- numpy.ndarray Ciphertext of the encoded plaintext of the floating-point data. See Also -------- eclib.gsw.encrypt eclib.gsw.encode """ return encrypt(params, pk, encode(params, x, scale))
[docs] def dec( params: PublicParameters, sk: SecretKey, c: NDArray[np.object_], scale: float ) -> ArrayLike: """ Decrypts and decodes a scalar, vector, or matrix ciphertext `c` using a secret key `sk`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. sk : eclib.regev.SecretKey Secret key used for decryption. c : numpy.ndarray Ciphertext to be decrypted and decoded. scale : float Scaling factor. Returns ------- array_like Decoded floating-point data of the decrypted plaintext. See Also -------- eclib.gsw.decrypt eclib.gsw.decode """ return decode(params, decrypt(params, sk, c), scale)
def _encrypt(params: PublicParameters, pk: PublicKey, m: int) -> NDArray[np.object_]: """ Encrypts a message `m` using a public key `pk`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. pk : eclib.regev.PublicKey Public key used for encryption. m : int Plaintext to be encrypted. Returns ------- numpy.ndarray Ciphertext of the plaintext. """ R = np.array( [[ru.get_rand(0, 2) for _ in range(params.N)] for _ in range(params.m)], dtype=object, ) G = _gadget(params) return (pk.B @ R + m * G) % params.q def _decrypt(params: PublicParameters, sk: SecretKey, c: NDArray[np.object_]) -> int: """ Decrypts a ciphertext `c` using a secret key `sk`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. sk : eclib.regev.SecretKey Secret key used for decryption. c : numpy.ndarray Ciphertext to be decrypted. Returns ------- int Decrypted plaintext. """ tmp = (np.block([1, -sk.s.T]) @ c[:, 0 : params.l]).reshape(-1) % params.q bits = np.zeros(params.l, dtype=object) lsb_sum = 0 bits[0] = floor(tmp[params.l - 1] / pow(2, params.l - 1) + 0.5) % 2 m = bits[0] for i in range(1, params.l): lsb_sum = lsb_sum / 2 + pow(2, params.l - 2) * bits[i - 1] bits[i] = ( floor((tmp[params.l - 1 - i] - lsb_sum) / pow(2, params.l - 1) + 0.5) % 2 ) m += (pow(2, i) * bits[i]) % params.q return m def _add( params: PublicParameters, c1: NDArray[np.object_], c2: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the addition of two plaintexts corresponding to ciphertexts `c1` and `c2`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. c1 : numpy.ndarray Ciphertext of the first plaintext. c2 : numpy.ndarray Ciphertext of the second plaintext. Returns ------- numpy.ndarray Ciphertext of the addition of the plaintexts. """ return (c1 + c2) % params.q def _mult( params: PublicParameters, c1: NDArray[np.object_], c2: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the product of two plaintexts corresponding to ciphertexts `c1` and `c2`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. c1 : numpy.ndarray Ciphertext of the first plaintext. c2 : numpy.ndarray Ciphertext of the second plaintext. Returns ------- numpy.ndarray Ciphertext of the product of the plaintexts. """ bit_decomposed = np.block( [_bitdecomp(params, c2[:, i]) for i in range(c2.shape[1])] ) return (c1 @ bit_decomposed) % params.q def _int_mult( params: PublicParameters, m: int, c: NDArray[np.object_] ) -> NDArray[np.object_]: """ Computes a ciphertext of the product of a plaintext `m` and another plaintext corresponding to a ciphertext `c`. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. m : int Plaintext to be multiplied. c : numpy.ndarray Ciphertext of a plaintext to be multiplied. Returns ------- numpy.ndarray Ciphertext of the product of the plaintexts. """ return (m * c) % params.q def _encode(params: PublicParameters, x: float, scale: float) -> int: """ Encodes a floating-point number `x` into a plaintext. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. x : float Floating-point number to be encoded. scale : float Scaling factor. Returns ------- int Encoded plaintext. Raises ------ ValueError If the encoded value is out of range (underflow or overflow). """ m = floor(x / scale + 0.5) if m < -((params.q - 1) // 2): raise ValueError("Underflow") elif m > (params.q // 2): raise ValueError("Overflow") else: return m % params.q def _decode(params: PublicParameters, m: int, scale: float) -> float: """ Decodes a plaintext `m` into a floating-point number. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. m : int Plaintext to be decoded. scale : float Scaling factor. Returns ------- float Decoded floating-point number. """ return (m - floor(m / params.q + 0.5) * params.q) * scale def _gadget(params: PublicParameters) -> NDArray[np.object_]: """ Constructs the gadget matrix for the GSW encryption scheme. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. Returns ------- numpy.ndarray The gadget matrix. """ g = 2 ** np.arange(params.l, dtype=object) return np.kron(np.identity(params.n + 1, dtype=object), g) def _bitdecomp(params: PublicParameters, v: ArrayLike) -> NDArray[np.object_]: """ Decomposes the input array `v` into its binary representation. Parameters ---------- params : eclib.gsw.PublicParameters Cryptosystem parameters. v : array_like Input array to be decomposed. Returns ------- numpy.ndarray Binary decomposition of the input array. Raises ------ ValueError If the input array is not one-dimensional. """ v = np.asarray(v, dtype=object) if v.ndim != 1: raise ValueError("Input array must be one-dimensional.") else: le_bin_repr = [np.binary_repr(x, params.l)[::-1] for x in v] return np.array( [ [int(le_bin_repr[i][j])] for i in range(len(le_bin_repr)) for j in range(params.l) ], dtype=object, )