mirror of
https://github.com/luxfi/lattice.git
synced 2026-09-24 00:25:44 +00:00
- Update module path to github.com/luxfi/lattice/v7 - Add GPU acceleration with Metal (macOS) and CUDA backends - Enhance threshold FHE operations - Revert to vanilla Apache 2.0 license (remove tfhe) - Update Go version to 1.25.5 - Merge upstream lattigo improvements
331 lines
13 KiB
Go
331 lines
13 KiB
Go
package rlwe
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
"github.com/luxfi/lattice/v7/ring/ringqp"
|
|
"github.com/luxfi/lattice/v7/utils"
|
|
"github.com/luxfi/lattice/v7/utils/sampling"
|
|
)
|
|
|
|
// KeyGenerator is a structure that stores the elements required to create new keys,
|
|
// as well as a memory buffer for intermediate values.
|
|
type KeyGenerator struct {
|
|
*Encryptor
|
|
pool *BufferPool
|
|
}
|
|
|
|
// NewKeyGenerator creates a new KeyGenerator, from which the secret and public keys, as well as [EvaluationKey].
|
|
func NewKeyGenerator(params ParameterProvider) *KeyGenerator {
|
|
return &KeyGenerator{
|
|
Encryptor: NewEncryptor(params, nil),
|
|
pool: NewPool(params.GetRLWEParameters().RingQP()),
|
|
}
|
|
}
|
|
|
|
// GenSecretKeyNew generates a new [SecretKey].
|
|
// Distribution is set according to [rlwe.Parameters.HammingWeight].
|
|
func (kgen KeyGenerator) GenSecretKeyNew() (sk *SecretKey) {
|
|
sk = NewSecretKey(kgen.params)
|
|
kgen.GenSecretKey(sk)
|
|
return
|
|
}
|
|
|
|
// GenSecretKey generates a [SecretKey].
|
|
// Distribution is set according to [rlwe.Parameters.HammingWeight].
|
|
func (kgen KeyGenerator) GenSecretKey(sk *SecretKey) {
|
|
kgen.genSecretKeyFromSampler(kgen.xsSampler, sk)
|
|
}
|
|
|
|
// GenSecretKeyWithHammingWeightNew generates a new [SecretKey] with exactly hw non-zero coefficients.
|
|
func (kgen *KeyGenerator) GenSecretKeyWithHammingWeightNew(hw int) (sk *SecretKey) {
|
|
sk = NewSecretKey(kgen.params)
|
|
kgen.GenSecretKeyWithHammingWeight(hw, sk)
|
|
return
|
|
}
|
|
|
|
// GenSecretKeyWithHammingWeight generates a [SecretKey] with exactly hw non-zero coefficients.
|
|
func (kgen KeyGenerator) GenSecretKeyWithHammingWeight(hw int, sk *SecretKey) {
|
|
Xs, err := ring.NewSampler(kgen.prng, kgen.params.RingQ(), ring.Ternary{H: hw}, false)
|
|
// Sanity check, this error should not happen.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
kgen.genSecretKeyFromSampler(Xs, sk)
|
|
}
|
|
|
|
func (kgen KeyGenerator) genSecretKeyFromSampler(sampler ring.Sampler, sk *SecretKey) {
|
|
|
|
ringQP := kgen.params.RingQP().AtLevel(sk.LevelQ(), sk.LevelP())
|
|
|
|
sampler.AtLevel(sk.LevelQ()).Read(sk.Value.Q)
|
|
|
|
if levelP := sk.LevelP(); levelP > -1 {
|
|
ringQP.ExtendBasisSmallNormAndCenter(sk.Value.Q, levelP, sk.Value.Q, sk.Value.P)
|
|
}
|
|
|
|
ringQP.NTT(sk.Value, sk.Value)
|
|
ringQP.MForm(sk.Value, sk.Value)
|
|
}
|
|
|
|
// GenPublicKeyNew generates a new [PublicKey] from the provided [SecretKey].
|
|
func (kgen KeyGenerator) GenPublicKeyNew(sk *SecretKey) (pk *PublicKey) {
|
|
pk = NewPublicKey(kgen.params)
|
|
kgen.GenPublicKey(sk, pk)
|
|
return
|
|
}
|
|
|
|
// GenPublicKey generates a [PublicKey] from the provided [SecretKey].
|
|
func (kgen KeyGenerator) GenPublicKey(sk *SecretKey, pk *PublicKey) {
|
|
if err := kgen.WithKey(sk).EncryptZero(Element[ringqp.Poly]{
|
|
MetaData: &MetaData{CiphertextMetaData: CiphertextMetaData{IsNTT: true, IsMontgomery: true}},
|
|
Value: []ringqp.Poly(pk.Value),
|
|
}); err != nil {
|
|
// Sanity check, this error should not happen.
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// GenKeyPairNew generates a new [SecretKey] and a corresponding [PublicKey].
|
|
// Distribution of the [SecretKey] set according to [rlwe.Parameters.HammingWeight].
|
|
func (kgen KeyGenerator) GenKeyPairNew() (sk *SecretKey, pk *PublicKey) {
|
|
sk = kgen.GenSecretKeyNew()
|
|
pk = kgen.GenPublicKeyNew(sk)
|
|
return
|
|
}
|
|
|
|
// GenRelinearizationKeyNew generates a new [EvaluationKey] that will be used to relinearize [Ciphertexts] during multiplication.
|
|
func (kgen KeyGenerator) GenRelinearizationKeyNew(sk *SecretKey, evkParams ...EvaluationKeyParameters) (rlk *RelinearizationKey) {
|
|
levelQ, levelP, BaseTwoDecomposition, compressed := ResolveEvaluationKeyParameters(kgen.params, evkParams)
|
|
|
|
degree := 1
|
|
if compressed {
|
|
degree = 0
|
|
}
|
|
|
|
rlk = &RelinearizationKey{EvaluationKey: EvaluationKey{GadgetCiphertext: *NewGadgetCiphertext(kgen.params, degree, levelQ, levelP, BaseTwoDecomposition)}}
|
|
kgen.GenRelinearizationKey(sk, rlk)
|
|
return
|
|
}
|
|
|
|
// GenRelinearizationKey generates an [EvaluationKey] that will be used to relinearize [Ciphertexts] during multiplication.
|
|
func (kgen KeyGenerator) GenRelinearizationKey(sk *SecretKey, rlk *RelinearizationKey) {
|
|
sk2 := kgen.pool.AtLevel(rlk.LevelQ()).GetBuffPoly()
|
|
defer kgen.pool.RecycleBuffPoly(sk2)
|
|
|
|
sk2.CopyLvl(rlk.LevelQ(), sk.Value.Q)
|
|
kgen.params.RingQ().AtLevel(rlk.LevelQ()).MulCoeffsMontgomery(*sk2, sk.Value.Q, *sk2)
|
|
kgen.genEvaluationKey(*sk2, sk.Value, &rlk.EvaluationKey)
|
|
}
|
|
|
|
// GenGaloisKeyNew generates a new [GaloisKey], enabling the automorphism X^{i} -> X^{i * galEl}.
|
|
func (kgen KeyGenerator) GenGaloisKeyNew(galEl uint64, sk *SecretKey, evkParams ...EvaluationKeyParameters) (gk *GaloisKey) {
|
|
levelQ, levelP, BaseTwoDecomposition, compressed := ResolveEvaluationKeyParameters(kgen.params, evkParams)
|
|
|
|
degree := 1
|
|
if compressed {
|
|
degree = 0
|
|
}
|
|
|
|
gk = &GaloisKey{
|
|
EvaluationKey: EvaluationKey{GadgetCiphertext: *NewGadgetCiphertext(kgen.params, degree, levelQ, levelP, BaseTwoDecomposition)},
|
|
NthRoot: kgen.params.GetRLWEParameters().RingQ().NthRoot(),
|
|
}
|
|
kgen.GenGaloisKey(galEl, sk, gk)
|
|
return
|
|
}
|
|
|
|
// GenGaloisKey generates a GaloisKey, enabling the automorphism X^{i} -> X^{i * galEl}.
|
|
func (kgen KeyGenerator) GenGaloisKey(galEl uint64, sk *SecretKey, gk *GaloisKey) {
|
|
|
|
skIn := sk.Value
|
|
|
|
ringQP := kgen.params.RingQP().AtLevel(gk.LevelQ(), gk.LevelP())
|
|
polyQP := kgen.pool.AtLevel(gk.LevelQ(), gk.LevelP())
|
|
|
|
skOut := polyQP.GetBuffPolyQP()
|
|
defer polyQP.RecycleBuffPolyQP(skOut)
|
|
|
|
ringQ := ringQP.RingQ
|
|
ringP := ringQP.RingP
|
|
|
|
// We encrypt [-a * pi_{k^-1}(sk) + sk, a]
|
|
// This enables to first apply the gadget product, re-encrypting
|
|
// a ciphetext from sk to pi_{k^-1}(sk) and then we apply pi_{k}
|
|
// on the ciphertext.
|
|
galElInv := kgen.params.ModInvGaloisElement(galEl)
|
|
|
|
index, err := ring.AutomorphismNTTIndex(ringQ.N(), ringQ.NthRoot(), galElInv)
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ringQ.AutomorphismNTTWithIndex(skIn.Q, index, skOut.Q)
|
|
|
|
if ringP != nil {
|
|
ringP.AutomorphismNTTWithIndex(skIn.P, index, skOut.P)
|
|
}
|
|
|
|
kgen.genEvaluationKey(skIn.Q, *skOut, &gk.EvaluationKey)
|
|
|
|
gk.GaloisElement = galEl
|
|
gk.NthRoot = ringQ.NthRoot()
|
|
}
|
|
|
|
// GenGaloisKeys generates the [GaloisKey] objects for all galois elements in galEls, and stores
|
|
// the resulting key for galois element i in gks[i].
|
|
// The galEls and gks parameters must have the same length.
|
|
func (kgen KeyGenerator) GenGaloisKeys(galEls []uint64, sk *SecretKey, gks []*GaloisKey) {
|
|
|
|
// Sanity check
|
|
if len(galEls) != len(gks) {
|
|
panic(fmt.Errorf("galEls and gks must have the same length"))
|
|
}
|
|
|
|
for i, galEl := range galEls {
|
|
if gks[i] == nil {
|
|
gks[i] = kgen.GenGaloisKeyNew(galEl, sk)
|
|
} else {
|
|
kgen.GenGaloisKey(galEl, sk, gks[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// GenGaloisKeysNew generates the [GaloisKey] objects for all galois elements in galEls, and
|
|
// returns the resulting keys in a newly allocated []*[GaloisKey].
|
|
func (kgen KeyGenerator) GenGaloisKeysNew(galEls []uint64, sk *SecretKey, evkParams ...EvaluationKeyParameters) (gks []*GaloisKey) {
|
|
levelQ, levelP, BaseTwoDecomposition, compressed := ResolveEvaluationKeyParameters(kgen.params, evkParams)
|
|
gks = make([]*GaloisKey, len(galEls))
|
|
for i, galEl := range galEls {
|
|
gks[i] = newGaloisKey(kgen.params, levelQ, levelP, BaseTwoDecomposition, compressed)
|
|
kgen.GenGaloisKey(galEl, sk, gks[i])
|
|
}
|
|
return
|
|
}
|
|
|
|
// GenEvaluationKeysForRingSwapNew generates the necessary evaluation keys to switch from a standard ring to to a conjugate invariant ring and vice-versa.
|
|
func (kgen KeyGenerator) GenEvaluationKeysForRingSwapNew(skStd, skConjugateInvariant *SecretKey, evkParams ...EvaluationKeyParameters) (stdToci, ciToStd *EvaluationKey) {
|
|
|
|
levelQ := utils.Min(skStd.Value.Q.Level(), skConjugateInvariant.Value.Q.Level())
|
|
|
|
skCIMappedToStandard := &SecretKey{Value: kgen.params.RingQP().AtLevel(levelQ, kgen.params.MaxLevelP()).NewPoly()}
|
|
kgen.params.RingQ().AtLevel(levelQ).UnfoldConjugateInvariantToStandard(skConjugateInvariant.Value.Q, skCIMappedToStandard.Value.Q)
|
|
|
|
if kgen.params.PCount() != 0 {
|
|
buffQ := kgen.pool.GetBuffPoly()
|
|
defer kgen.pool.RecycleBuffPoly(buffQ)
|
|
ExtendBasisSmallNormAndCenterNTTMontgomery(kgen.params.RingQ(), kgen.params.RingP(), skCIMappedToStandard.Value.Q, *buffQ, skCIMappedToStandard.Value.P)
|
|
}
|
|
|
|
levelQ, levelP, BaseTwoDecomposition, compressed := ResolveEvaluationKeyParameters(kgen.params, evkParams)
|
|
|
|
stdToci = newEvaluationKey(kgen.params, levelQ, levelP, BaseTwoDecomposition, compressed)
|
|
kgen.GenEvaluationKey(skStd, skCIMappedToStandard, stdToci)
|
|
|
|
ciToStd = newEvaluationKey(kgen.params, levelQ, levelP, BaseTwoDecomposition, compressed)
|
|
kgen.GenEvaluationKey(skCIMappedToStandard, skStd, ciToStd)
|
|
|
|
return
|
|
}
|
|
|
|
// GenEvaluationKeyNew generates a new [EvaluationKey], that will re-encrypt a [Ciphertext] encrypted under the input key into the output key.
|
|
// If the ringDegree(skOutput) > ringDegree(skInput), generates [-a*SkOut + w*P*skIn_{Y^{N/n}} + e, a] in X^{N}.
|
|
// If the ringDegree(skOutput) < ringDegree(skInput), generates [-a*skOut_{Y^{N/n}} + w*P*skIn + e_{N}, a_{N}] in X^{N}.
|
|
// Else generates [-a*skOut + w*P*skIn + e, a] in X^{N}.
|
|
// The output [EvaluationKey] is always given in max(N, n).
|
|
// If evkParams is void, the output [EvaluationKey] will be in the moduli given by the parameters stored in the [KeyGenerator] kgen.
|
|
// When re-encrypting a [Ciphertext] from Y^{N/n} to X^{N}, the Ciphertext must first be mapped to X^{N}
|
|
// using [SwitchCiphertextRingDegreeNTT](ctSmallDim, nil, ctLargeDim).
|
|
// When re-encrypting a [Ciphertext] from X^{N} to Y^{N/n}, the output of the re-encryption is in still X^{N} and
|
|
// must be mapped Y^{N/n} using [SwitchCiphertextRingDegreeNTT](ctLargeDim, ringQLargeDim, ctSmallDim).
|
|
func (kgen KeyGenerator) GenEvaluationKeyNew(skInput, skOutput *SecretKey, evkParams ...EvaluationKeyParameters) (evk *EvaluationKey) {
|
|
levelQ, levelP, BaseTwoDecomposition, compressed := ResolveEvaluationKeyParameters(kgen.params, evkParams)
|
|
evk = newEvaluationKey(kgen.params, levelQ, levelP, BaseTwoDecomposition, compressed)
|
|
kgen.GenEvaluationKey(skInput, skOutput, evk)
|
|
return
|
|
}
|
|
|
|
// GenEvaluationKey generates an [EvaluationKey], that will re-encrypt a [Ciphertext] encrypted under the input key into the output key.
|
|
// If the ringDegree(skOutput) > ringDegree(skInput), generates [-a*SkOut + w*P*skIn_{Y^{N/n}} + e, a] in X^{N}.
|
|
// If the ringDegree(skOutput) < ringDegree(skInput), generates [-a*skOut_{Y^{N/n}} + w*P*skIn + e_{N}, a_{N}] in X^{N}.
|
|
// Else generates [-a*skOut + w*P*skIn + e, a] in X^{N}.
|
|
// The output [EvaluationKey] is always given in max(N, n) and in the moduli of the output [EvaluationKey].
|
|
// When re-encrypting a [Ciphertext] from Y^{N/n} to X^{N}, the [Ciphertext] must first be mapped to X^{N}
|
|
// using [SwitchCiphertextRingDegreeNTT](ctSmallDim, nil, ctLargeDim).
|
|
// When re-encrypting a [Ciphertext] from X^{N} to Y^{N/n}, the output of the re-encryption is in still X^{N} and
|
|
// must be mapped Y^{N/n} using [SwitchCiphertextRingDegreeNTT](ctLargeDim, ringQLargeDim, ctSmallDim).
|
|
func (kgen KeyGenerator) GenEvaluationKey(skInput, skOutput *SecretKey, evk *EvaluationKey) {
|
|
|
|
ringQ := kgen.params.RingQ()
|
|
ringP := kgen.params.RingP()
|
|
|
|
buffSkOut := kgen.pool.GetBuffPolyQP()
|
|
defer kgen.pool.RecycleBuffPolyQP(buffSkOut)
|
|
buffSkIn := kgen.pool.GetBuffPoly()
|
|
defer kgen.pool.RecycleBuffPoly(buffSkIn)
|
|
// Maps the smaller key to the largest with Y = X^{N/n}.
|
|
ring.MapSmallDimensionToLargerDimensionNTT(skOutput.Value.Q, buffSkOut.Q)
|
|
|
|
// Extends the modulus P of skOutput to the one of skInput
|
|
buffQ := kgen.pool.AtLevel(0).GetBuffPoly()
|
|
defer kgen.pool.RecycleBuffPoly(buffQ)
|
|
if levelP := evk.LevelP(); levelP != -1 {
|
|
ExtendBasisSmallNormAndCenterNTTMontgomery(ringQ, ringP.AtLevel(levelP), buffSkOut.Q, *buffQ, buffSkOut.P)
|
|
}
|
|
|
|
// Maps the smaller key to the largest dimension with Y = X^{N/n}.
|
|
ring.MapSmallDimensionToLargerDimensionNTT(skInput.Value.Q, *buffSkIn)
|
|
ExtendBasisSmallNormAndCenterNTTMontgomery(ringQ, ringQ.AtLevel(skOutput.Value.Q.Level()), *buffSkIn, *buffQ, *buffSkIn)
|
|
|
|
kgen.genEvaluationKey(*buffSkIn, *buffSkOut, evk)
|
|
}
|
|
|
|
func (kgen KeyGenerator) genEvaluationKey(skIn ring.Poly, skOut ringqp.Poly, evk *EvaluationKey) {
|
|
|
|
enc := kgen.WithKey(&SecretKey{Value: skOut})
|
|
|
|
// For a compressed evaluation key, a seed is created and stored in the EvaluationKey struct
|
|
// struct while an uncompressed key uses an ephemeral seed.
|
|
if evk.IsCompressed() {
|
|
var seed [32]byte
|
|
if n, err := kgen.prng.Read(seed[:]); n != 32 || err != nil {
|
|
panic(fmt.Errorf("unable to sample evaluation key seed"))
|
|
}
|
|
evk.Seed = &seed
|
|
|
|
sampler, err := sampling.NewKeyedPRNG(seed[:])
|
|
if err != nil {
|
|
panic(fmt.Errorf("sampling.NewKeyedPRNG: %w", err))
|
|
}
|
|
|
|
enc = enc.withKeyedUniformSampling(sampler)
|
|
}
|
|
|
|
// Samples an encryption of zero for each element of the EvaluationKey.
|
|
for i := 0; i < len(evk.Value); i++ {
|
|
for j := 0; j < len(evk.Value[i]); j++ {
|
|
// evk[i][j] = (-a*sk + e, a) if the degree of degree of the GadgetCiphertext is 1
|
|
// evk[i][j] = (-a*sk + e) if the degree is 0
|
|
if err := enc.EncryptZero(Element[ringqp.Poly]{
|
|
MetaData: &MetaData{CiphertextMetaData: CiphertextMetaData{IsNTT: true, IsMontgomery: true}},
|
|
Value: []ringqp.Poly(evk.Value[i][j]),
|
|
}); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adds the plaintext (input-key) to the EvaluationKey.
|
|
buffQ := kgen.pool.AtLevel(evk.GadgetCiphertext.LevelQ()).GetBuffPoly()
|
|
defer kgen.pool.RecycleBuffPoly(buffQ)
|
|
|
|
if err := AddPolyTimesGadgetVectorToGadgetCiphertext(skIn, []GadgetCiphertext{evk.GadgetCiphertext}, *kgen.params.RingQP(), *buffQ); err != nil {
|
|
// Sanity check, this error should not happen.
|
|
panic(err)
|
|
}
|
|
}
|