commit 09a5754cf9ee2d40039646f742a461978906c661 Author: Sam Hadow Date: Wed May 21 22:51:23 2025 +0200 initial commit diff --git a/README b/README new file mode 100644 index 0000000..efe528b --- /dev/null +++ b/README @@ -0,0 +1,137 @@ + An implementation of the DGHV fully homomorphic scheme + +This is an implementation of the DGHV fully homomorphic scheme with +compressed public-key. This implementation is described in the +following article: + +[1] J.S. Coron, D. Naccache and M. Tibouchi, "Public-key Compression and +Modulus Switching for Fully Homomorphic Encryption over the Integers", +Proceedings of Eurocrypt 2012. + +available at http://eprint.iacr.org/2011/440 + +The implementation is done with the SAGE 4.7.2 mathematical library +under Python, available at http://www.sagemath.org/. + +WHAT IS FULLY HOMOMORPHIC ENCRYPTION ? +-------------------------------------- + +An encryption scheme is fully homomorphic when it is possible to +perform implicit addition and multiplication of plaintexts while +manipulating only ciphertexts. The first construction of a fully +homomorphic encryption (FHE) scheme was described by Gentry in 2009. + +WHAT IS THE DGHV SCHEME ? +------------------------- + +The DGHV scheme is a FHE scheme published in: + +[2] M. van Dijk, C. Gentry, S. Halevi and V. Vaikuntanathan, "Fully + Homomorphic Encryption over the Integers". Proceedings of Eurocrypt + 2010. + +and available at http://eprint.iacr.org/2009/616 + +In DGHV a ciphertext has the form: + +c= q*p + 2*r + m + +where p is the secret-key, m is the bit plaintext (m=0 or 1), q is a +large random, and r is a small random. + +To decrypt, compute m=(c mod p) mod 2. It is easy to see that +decryption works as long as the noise 2*r is smaller than p. + +Given two ciphertexts +c1= q1*p + 2*r1 + m1 +c2= q2*p + 2*r2 + m2 + +we have: + +c1+c2=(q1+q2)*p+2*(r1+r2)+m1+m2 + +Therefore one can obtain the encryption of m1+m2 (mod 2)=m1 xor m2 simply by +computing c1+c2. + +Similarly we have: + +c1*c2=q12*p+2*(2*r1*r2+r1*m2+r2*m1)+m1*m2 + +Therefore one can obtain the encryption of m1*m2 simply by computing +c1*c2. However one gets a new ciphertext with noise roughly +twice larger than in the original ciphertexts c1 and c2. Since the +noise must remain below p, the number of permitted multiplications on +ciphertexts is therefore limited. This is called a somewhat +homomorphic encryption scheme. + +To obtain a fully homomorphic encryption scheme, i.e. unlimited +addition and multiplication on ciphertexts, one must be able to reduce +the amount of noise in a ciphertext; this is called a ciphertext +refresh. + +Gentry's key idea to refresh a ciphertext is to homomorphically +evaluate the decryption circuit on the ciphertext bits, using an +encryption of the secret-key bits. This is called bootstrapping. +Then instead of getting the plaintext bit (as we would get if we +would evaluate the decryption circuit with the secret-key bits in +clear), we get an __encryption__ of the plaintext bit, i.e. a new +ciphertext for the same plaintext. Now if the decryption circuit has a +small enough depth, then the amount of noise in the new ciphertext can +be actually smaller than in the original plaintext, hence a ciphertext refresh. + +However the previous decryption algorithm m=(c mod p) mod 2 does not +have a small depth, therefore the decryption procedure must be "squashed" so +that it can be expressed as a low depth circuit. How this is done in +explained in [2]. + +So far we have only described a secret-key encryption scheme, i.e. to +encrypt one must know the secret-key p. However it is easy to obtain a +public-key encryption scheme. For this generate a public set of +ciphertexts xi which are all different encryptions of 0: + +xi=qi*p+2*ri + +and to encrypt a bit m compute: + +c=m+2*r+random_subset_sum(xi) + +It is easy to see that c is indeed an encryption of the bit m. + +WHAT IS IMPLEMENTED ? +--------------------- + +We provide an implementation of the DGHV scheme with the fully +homomorphic capability, i.e. we implement the key generation, +encryption, decryption, add, mult and ciphertext refresh procedures. + +The implementation is done using the Sage 4.7.2 mathematical library +under Python, available at http://www.sagemath.org/ + +First run sage, then type: + +sage: load "dghv.sage" +sage: testPkRecrypt() + +The testPkRecrypt() function demonstrates key generation, addition and +multiplication of ciphertexts, and ciphertext refresh. It runs for +four sets of parameters (toy, small, medium and large), as described +in [1]. + + +WHAT IS A COMPRESSED PUBLIC-KEY ? +--------------------------------- + +In DGHV the ciphertext size must be huge to prevent attacks based on +lattice reduction algorithms; at least 10^7 bits. The secret p is +comparatively smaller, roughly 2000 bits. Since roughly 10^4 +ciphertexts xi must be included in the public-key, that would give a +public-key size of 10^11 bits, i.e. 12.5 GB. + +To reduce the public-key size we implement the following technique +described in [1]. Instead of generating xi as xi=qi*p+2*ri, one first +generates a pseudo-random Xi of the same size, and computes a small +correction di such that xi=Xi-di is small modulo p. Then only these +small corrections need to be stored in the public-key, with the seed +of the PRNG. Using this compression technique the public-key size +becomes 10^4*(2*10^3)=2*10^7 bits, i.e. 2.5 MB, which is more +manageable. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d246283 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +DGHV sage implementation ported to python3 from the original python2 version available here: https://github.com/coron/fhe diff --git a/dghv.sage b/dghv.sage new file mode 100644 index 0000000..192d1b4 --- /dev/null +++ b/dghv.sage @@ -0,0 +1,234 @@ +# +# dghv.sage: an implementation of DGHV-FHE with compressed public-key +# +# as described in: +# +# J.S. Coron, D. Naccache and M. Tibouchi, "Public-key Compression and Modulus +# Switching for Fully Homomorphic Encryption over the Integers" +# +# available at http://eprint.iacr.org/2011/440 +# +# Copyright (c) 2012 Jean-Sebastien Coron +# and Mehdi Tibouchi +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published +# by the Free Software Foundation. + +load scalprod.spyx +load utils.sage +from itertools import chain, islice, count +from time import time + +theta = 15 +n = 4 + +class Pk(object): + "The PK of the DGHV somewhat homomorphic scheme, without the xi's" + def __init__(self, rho, eta, gam, modx0=True, verbose=True, *args, **kwargs): + print(" Pk: generation of p") + self.rho, self.eta, self.gam, self.modx0 = rho, eta, gam, modx0 + self.p = random_prime(2^self.eta, lbound=2^(self.eta-1), proof=False) + if self.modx0: + self.x0 = self.p * RandomOdd(self.gam - self.eta) + + def encrypt(self, m): + return self.p * ZZ.random_element(2^(self.gam - self.eta - 1)) + 2 * ZZ.random_element(-2^self.rho + 1, 2^self.rho) + m + + def noise(self, c): + return modNear(c, self.p) + + def decrypt(self, c): + return mod(self.noise(c), 2) + + def add(self, c1, c2): + return self.modx0 and mod(c1 + c2, self.x0).lift() or (c1 + c2) + + def mult(self, c1, c2): + return self.modx0 and mod(c1 * c2, self.x0).lift() or (c1 * c2) + + def __repr__(self): + return "" % (self.rho, self.eta, self.gam) + +class Ciphertext(): + def __init__(self, val_, pk_, degree_=1): + self.val, self.pk, self.degree = val_, pk_, degree_ + + @staticmethod + def encrypt(pk, m=None): + if m == None: + m = ZZ.random_element(2) + return Ciphertext(pk.encrypt(m), pk) + + def noise(self): + return self.pk.noise(self.val) + + def decrypt(self, verbose=False): + t = cputime(subprocesses=True) + m = self.pk.decrypt(self.val) + if verbose: + print("Decrypt", cputime(subprocesses=True) - t) + return m + + def __repr__(self): + return "" % (self.decrypt(), self.val.nbits(), self.noise().nbits(), self.degree) + + def __add__(self, x): + return self.__class__(self.pk.add(self.val, x.val), self.pk, max(self.degree, x.degree)) + + def __mul__(self, x): + return self.__class__(self.pk.mult(self.val, x.val), self.pk, self.degree + x.degree) + + def scalmult(self, x): + if isinstance(x, array): + return array([self.__class__(self.val * xi, self.pk, self.degree) for xi in x]) + else: + return self.__class__(self.val * x, self.pk, self.degree) + + def expand(self): + return self.pk.expand(self.val) + +class PRIntegers: + "A list of pseudo-random integers." + def __init__(self, gam, ell): + self.gam, self.ell = gam, ell + self.li = [None for i in range(self.ell)] + set_random_seed() + self.se = initial_seed() + + def __getitem__(self, i): + return self.li[i] + + def __setitem__(self, i, val): + self.li[i] = val + + def __iter__(self): + set_random_seed(self.se) + for i in range(self.ell): + a = ZZ.random_element(2^self.gam) + if self.li[i] != None: + yield self.li[i] + else: + yield a + set_random_seed() + +class PRIntegersDelta(PRIntegers): + """A list of pseudo-random integers, with their delta corrections""" + def __iter__(self): + return (c + d for c, d in zip(PRIntegers.__iter__(self), self.delta)) + + def ciphertexts(self, pk): + return (Ciphertext(cd, pk) for cd in self) + + @staticmethod + def encrypt(pk, v): + pr = PRIntegersDelta(pk.gam, len(v)) + r = [ZZ.random_element(-2^pk.rho + 1, 2^pk.rho) for i in range(len(v))] + pr.delta = [0 for i in range(len(v))] + + temp = [-mod(xi, pk.p) for xi in PRIntegers.__iter__(pr)] + pr.delta = [te.lift() + 2 * ri + vi for te, ri, vi in zip(temp, r, v)] + return pr + +class PkRecrypt(Pk): + "The Pk of the DGHV scheme, with the xi's, the yi's and the encrypted secret-key bits" + def __init__(self, rho, eta, gam, Theta, pkRecrypt=None, *args, **kwargs): + t = cputime(subprocesses=True) + super(PkRecrypt, self).__init__(rho, eta, gam, *args, **kwargs) + self.kap = 64 * (self.gam // 64 + 1) - 1 + self.Theta = Theta + self.alpha, self.tau = kwargs['alpha'], kwargs['tau'] + + xp = QuotientNear(2^self.kap, self.p) + B = self.Theta // theta + + assert self.Theta % theta == 0, "Theta must be a multiple of theta" + print(" PkRecrypt: generation of s") + self.s = [1] + [0 for i in range(B - 1)] + sum2([randSparse(B, 1) for i in range(theta - 1)]) + + t2 = cputime(subprocesses=True) + self.y = PRIntegers(self.kap, self.Theta) + self.y[0] = 0 + self.y[0] = mod(xp - prodScal(self.s, self.y), 2^(self.kap + 1)).lift() + assert mod(prodScal(self.s, self.y), 2^(self.kap + 1)) == mod(xp, 2^(self.kap + 1)), "Equality not valid" + print(" PkRecrypt: generation of yis", cputime(subprocesses=True) - t2) + + t2 = cputime(subprocesses=True) + self.x = PRIntegersDelta.encrypt(self, [0 for i in range(self.tau)]) + print(" PkRecrypt: generation of xis", cputime(subprocesses=True) - t2) + + t2 = cputime(subprocesses=True) + self.pkRecrypt = pkRecrypt + if not self.pkRecrypt: + self.pkRecrypt = self + self.se = PRIntegersDelta.encrypt(self.pkRecrypt, self.s) + print(" PkRecrypt: generation of seis", cputime(subprocesses=True) - t2) + + print(" PkRecrypt: genkey", cputime(subprocesses=True) - t) + + def encrypt_pk(self, m=None, verbose=True): + t = cputime(subprocesses=True) + if m == None: + m = ZZ.random_element(2) + rhop = self.eta - self.rho + + f = [Ciphertext(ZZ.random_element(2^self.alpha), self) for i in range(self.tau)] + + c = sum2(ci * fi for ci, fi in zip(self.x.ciphertexts(self), f)) + Ciphertext(ZZ.random_element(2^rhop), self) + Ciphertext(m, self) + if verbose: + print("Encrypt", cputime(subprocesses=True) - t) + c.degree = 1 + return c + + def expand(self, c, verbose=True): + m = 2^(n + 1) - 1 + t = cputime(subprocesses=True) + + ce = [(((directProd(c, yi, self.kap) >> (32 - (n + 2))) + 1) >> 1) & m for yi in self.y] + + if verbose: + print("expand", cputime(subprocesses=True) - t) + return ce + + def recrypt(self, c, verbose=False): + t = cputime(subprocesses=True) + B = self.Theta // theta + cebin = [array(toBinary(cei, n + 1)) for cei in c.expand()] + + sec = (c.__class__(ci, self.pkRecrypt) for ci in self.se) + + li = (ski.scalmult(cei) for ski, cei in zip(sec, cebin)) + ly = [sum2(islice(li, B)) for i in range(theta)] + + res = reduceSteps(sumBinary, ly, verbose) + v = res[-1] + res[-2] + v.val += c.val & 1 + if verbose: + print("recrypt", cputime(subprocesses=True) - t) + return v + +def testPkRecrypt(): + toy = {'ty': "toy", 'lam': 42, 'rho': 26, 'eta': 988, 'gam': 147456, 'Theta': 150, 'pksize': 0.076519, 'seclevel': 42.0, 'alpha': 936, 'tau': 158} + small = {'ty': "small", 'lam': 52, 'rho': 41, 'eta': 1558, 'gam': 843033, 'Theta': 555, 'pksize': 0.437567, 'seclevel': 52.0, 'alpha': 1476, 'tau': 572} + medium = {'ty': "medium", 'lam': 62, 'rho': 56, 'eta': 2128, 'gam': 4251866, 'Theta': 2070, 'pksize': 2.207241, 'seclevel': 62.0, 'alpha': 2016, 'tau': 2110} + large = {'ty': "large", 'lam': 72, 'rho': 71, 'eta': 2698, 'gam': 19575950, 'Theta': 7965, 'pksize': 10.303797, 'seclevel': 72.0, 'alpha': 2556, 'tau': 7659} + + for param in [toy, small, medium, large]: + ty, rho, eta, gam, Theta = [param[x] for x in ['ty', 'rho', 'eta', 'gam', 'Theta']] + print("type=", ty, "lam=", param['lam'], "rho=", rho, "eta=", eta, "gamma=", gam, "Theta=", Theta) + pk = PkRecrypt(rho, eta, gam, Theta, tau=param['tau'], alpha=param['alpha']) + + c1 = Ciphertext.encrypt(pk) + c2 = Ciphertext.encrypt(pk) + print("c1=", c1) + print("c2=", c2) + print("c1+c2=", c1 + c2) + t = cputime(subprocesses=True) + print("c1*c2=", c1 * c2) + print("tmult:", cputime(subprocesses=True) - t) + + c = pk.encrypt_pk() + c.decrypt(verbose=True) + print("c=", c) + newc = pk.recrypt(c, verbose=True) + print("newc=", newc) diff --git a/directscal.c b/directscal.c new file mode 100644 index 0000000..80a45b1 --- /dev/null +++ b/directscal.c @@ -0,0 +1,47 @@ +/* +* directscale.c: faster implementation of ciphertext expand +* +* Copyright (c) 2012 Mehdi Tibouchi +* +* This program is free software; you can redistribute it and/or modify it +* under the terms of the GNU General Public License version 2 as published +* by the Free Software Foundation. +*/ + +#include + +#define w (GMP_NUMB_BITS/2) +#define _BOTMASK ((1ul << w)-1) +#define _TOPMASK (~_BOTMASK) +#define BOT(x) ((x) & _BOTMASK) +#define TOP(x) ((x) >> w) +#define LIMB(z,i) (((i)<((z)->_mp_size))?((z)->_mp_d[i]):(0L)) +#define BOTL(z,i) (BOT(LIMB(z,i))) +#define TOPL(z,i) (TOP(LIMB(z,i))) +#define HLIMB(z,j) ((j&1)?(TOPL(z,j>>1)):(BOTL(z,j>>1))) + +unsigned getGMP_NUMB_BITS() +{ + return GMP_NUMB_BITS; +} + +unsigned long directScal(unsigned long kap, mpz_t cz, mpz_t yz) +{ + unsigned long nW=(kap+1)/(2*w), val=0, i; + + if(nW*w*2 != kap+1) + return 0; + + for(i = 0; i < nW-1; i++) { + val += BOTL(cz,i) * LIMB(yz,nW-1-i); + val += (BOTL(cz,i) * TOPL(yz,nW-2-i)) >> w; + val += TOPL(cz,i) * ((BOTL(yz,nW-1-i) << w) + TOPL(yz,nW-2-i)); + val += (TOPL(cz,i) * BOTL(yz,nW-2-i)) >> w; + } + + val += BOTL(cz,nW-1) * LIMB(yz,0); + val += TOPL(cz,nW-1) * (BOTL(yz,0) << w); + + return val; +} + diff --git a/scalprod.spyx b/scalprod.spyx new file mode 100644 index 0000000..c51b346 --- /dev/null +++ b/scalprod.spyx @@ -0,0 +1,55 @@ +# +# scalprod.spyx: faster implementation of ciphertext expand +# +# Copyright (c) 2012 Jean-Sebastien Coron +# and Mehdi Tibouchi +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published +# by the Free Software Foundation. + +from sage.libs.gmp.all cimport mpz_t, mpz_export +from sage.rings.integer cimport Integer +from sage.misc.misc import cputime +from sage.all import ZZ +from libc.stdlib cimport calloc, free + +cdef extern from "directscal.c": + unsigned long directScal(unsigned long kap, mpz_t c, mpz_t y) + unsigned getGMP_NUMB_BITS() + +def directProd(c, y, kap): + cdef Integer yz = y + cdef Integer cz = c + return ZZ(directScal(kap, cz.value, yz.value)) >> (getGMP_NUMB_BITS() - 32) + +def partialProd(c, y, kap): + cdef int w = sizeof(int) * 4 # 16 bits + cdef int nw = (kap + 1) // w + assert nw * w == kap + 1 + + cdef Integer yz = y + cdef size_t ny + cdef unsigned int* yw = calloc(nw, sizeof(int)) + + cdef int order = -1 # least significant word first + cdef int endian = 0 + cdef size_t nails = sizeof(int) * 4 + mpz_export(yw, &ny, order, sizeof(int), endian, nails, yz.value) + assert ny <= nw + + cdef Integer cz = c + cdef size_t nc + cdef unsigned int* cw = calloc(nw, sizeof(int)) + mpz_export(cw, &nc, order, sizeof(int), endian, nails, cz.value) + assert nc <= nw + + cdef unsigned int val = 0 + for i in range(nw - 2): + val += cw[i] * ((yw[nw-1-i] << w) + yw[nw-2-i]) + ((cw[i] * yw[nw-3-i]) >> w) + + val += cw[nw-2] * ((yw[1] << w) + yw[0]) + cw[nw-1] * (yw[0] << w) + + free(cw) + free(yw) + return ZZ(val) diff --git a/utils.sage b/utils.sage new file mode 100644 index 0000000..0d94d22 --- /dev/null +++ b/utils.sage @@ -0,0 +1,86 @@ +# +# utils.sage +# +# Copyright (c) 2012 Jean-Sebastien Coron +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 as published +# by the Free Software Foundation. + + +from itertools import chain, islice, zip_longest +from sys import stdout + +def RandomOdd(n): + return 2 * (2^(n-2) + ZZ.random_element(2^(n-2))) + 1 + +def sum2(li): + empty = True + for x in li: + if empty: + res = x + empty = False + else: + res = res + x + if empty: + return 0 + else: + return res + +class array(list): + def __mul__(self, x): + return array([x * ci for ci in self]) + + def __add__(self, xx): + return array([ai + xi for ai, xi in zip(self, xx)]) + +def reduceSteps(f, li, verbose=True): + i = 0 + res = li[0] + if verbose: + print(i, end=' ') + for x in li[1:]: + stdout.flush() + i = i + 1 + res = f(res, x) + if verbose: + print(i, end=' ') + if verbose: + print() + return res + +def sumBinary(a, b): + "Computes the sum of the binary vectors a and b, modulo 2^n where n is the length of the vectors a and b" + c = [a[0] + b[0]] + carry = a[0] * b[0] + + for i in range(1, len(a) - 1): + carry2 = (a[i] + b[i]) * carry + a[i] * b[i] + c.append(a[i] + b[i] + carry) + carry = carry2 + + c.append(a[-1] + b[-1] + carry) + return c + +def QuotientNear(a, b): + "Gives the nearest integer to a/b" + return (2 * a + b) // (2 * b) + +def modNear(a, b): + "Computes a mod b with a in ]-b/2,b/2]" + return a - b * QuotientNear(a, b) + +def toBinary(x, l): + "Converts a positive integer x into binary with l digits" + return (x + 2^l).digits(2)[:-1] + +def prodScal(x, y): + return sum2((xi * yi for xi, yi in zip(x, y))) + +def randSparse(n, h): + v = [0 for i in range(n)] + while sum(v) < h: + i = int(ZZ.random_element(n)) + if v[i] == 0: + v[i] = 1 + return v