add NonInteractiveBatchedFheUInt8s for non-interactive MPC; Make multi-party decyrption protocol independent of bool evaluator
@@ -2070,41 +2070,6 @@ )
}) } - pub(super) fn multi_party_decryption_share<K: InteractiveMultiPartyClientKey<Element = i32>>( - &self, - lwe_ct: &M::R, - client_key: &K, - ) -> <M as Matrix>::MatElement { - assert!(lwe_ct.as_ref().len() == self.pbs_info.parameters.rlwe_n().0 + 1); - let modop = &self.pbs_info.rlwe_modop; - let mut neg_s = - M::R::try_convert_from(&client_key.sk_rlwe(), &self.pbs_info.parameters.rlwe_q()); - modop.elwise_neg_mut(neg_s.as_mut()); - - let mut neg_sa = M::MatElement::zero(); - izip!(lwe_ct.as_ref().iter().skip(1), neg_s.as_ref().iter()).for_each(|(ai, nsi)| { - neg_sa = modop.add(&neg_sa, &modop.mul(ai, nsi)); - }); - - let e = DefaultSecureRng::with_local_mut(|rng| { - RandomGaussianElementInModulus::random(rng, self.pbs_info.parameters.rlwe_q()) - }); - let share = modop.add(&neg_sa, &e); - - share - } - - pub(crate) fn multi_party_decrypt(&self, shares: &[M::MatElement], lwe_ct: &M::R) -> bool { - let modop = &self.pbs_info.rlwe_modop; - let mut sum_a = M::MatElement::zero(); - shares - .iter() - .for_each(|share_i| sum_a = modop.add(&sum_a, &share_i)); - - let encoded_m = modop.add(&lwe_ct.as_ref()[0], &sum_a); - self.pbs_info.parameters.rlwe_q().decode(encoded_m) - } - pub fn sk_encrypt<K: SinglePartyClientKey<Element = i32>>( &self, m: bool,
@@ -99,6 +99,7 @@ out
} } + #[cfg(feature = "interactive_mp")] impl<E> InteractiveMultiPartyClientKey for ClientKey<[u8; 32], E> { type Element = i32; fn sk_lwe(&self) -> Vec<Self::Element> {@@ -109,6 +110,7 @@ <Self as SinglePartyClientKey>::sk_rlwe(&self)
} } + #[cfg(feature = "non_interactive_mp")] impl<E> NonInteractiveMultiPartyClientKey for ClientKey<[u8; 32], E> { type Element = i32; fn sk_lwe(&self) -> Vec<Self::Element> {
@@ -205,25 +205,6 @@ };
type Mat = Vec<Vec<u64>>; - impl<E> MultiPartyDecryptor<bool, <Mat as Matrix>::R> for super::keys::ClientKey<[u8; 32], E> { - type DecryptionShare = <Mat as Matrix>::MatElement; - - /// Generate multi-party decryption share for LWE ciphertext `c` - fn gen_decryption_share(&self, c: &<Mat as Matrix>::R) -> Self::DecryptionShare { - BoolEvaluator::with_local(|e| e.multi_party_decryption_share(c, self)) - } - - /// Aggregate mult-party decryptions shares of all parties, decrypt LWE - /// ciphertext `c`, and return the bool plaintext - fn aggregate_decryption_shares( - &self, - c: &<Mat as Matrix>::R, - shares: &[Self::DecryptionShare], - ) -> bool { - BoolEvaluator::with_local(|e| e.multi_party_decrypt(shares, c)) - } - } - impl SampleExtractor<<Mat as Matrix>::R> for Mat { /// Sample extract coefficient at `index` as a LWE ciphertext from RLWE /// ciphertext `Self`
@@ -1,4 +1,4 @@
-use std::{cell::RefCell, ops::Mul, sync::OnceLock}; +use std::{cell::RefCell, sync::OnceLock}; use crate::{ backend::{ModularOpsU64, ModulusPowerOf2},@@ -184,9 +184,13 @@
mod impl_enc_dec { use crate::{ bool::evaluator::BoolEncoding, + multi_party::{ + multi_party_aggregate_decryption_shares_and_decrypt, multi_party_decryption_share, + }, pbs::{sample_extract, PbsInfo}, rgsw::public_key_encrypt_rlwe, - Encryptor, Matrix, MatrixEntity, RowEntity, + utils::TryConvertFrom1, + Encryptor, Matrix, MatrixEntity, MultiPartyDecryptor, RowEntity, }; use itertools::Itertools; use num_traits::{ToPrimitive, Zero};@@ -254,14 +258,50 @@ lwe
}) } } + + impl<K> MultiPartyDecryptor<bool, <Mat as Matrix>::R> for K + where + K: InteractiveMultiPartyClientKey, + <Mat as Matrix>::R: + TryConvertFrom1<[K::Element], CiphertextModulus<<Mat as Matrix>::MatElement>>, + { + type DecryptionShare = <Mat as Matrix>::MatElement; + + fn gen_decryption_share(&self, c: &<Mat as Matrix>::R) -> Self::DecryptionShare { + BoolEvaluator::with_local(|e| { + DefaultSecureRng::with_local_mut(|rng| { + multi_party_decryption_share( + c, + self.sk_rlwe().as_slice(), + e.pbs_info().modop_rlweq(), + rng, + ) + }) + }) + } + + fn aggregate_decryption_shares( + &self, + c: &<Mat as Matrix>::R, + shares: &[Self::DecryptionShare], + ) -> bool { + BoolEvaluator::with_local(|e| { + let noisy_m = multi_party_aggregate_decryption_shares_and_decrypt( + c, + shares, + e.pbs_info().modop_rlweq(), + ); + + e.pbs_info().rlwe_q().decode(noisy_m) + }) + } + } } #[cfg(test)] mod tests { - use std::thread::panicking; - use itertools::Itertools; - use rand::{thread_rng, RngCore}; + use rand::{thread_rng, Rng, RngCore}; use crate::{ bool::{@@ -269,7 +309,7 @@ evaluator::BoolEncoding,
keys::tests::{ideal_sk_rlwe, measure_noise_lwe}, BooleanGates, }, - Encryptor, MultiPartyDecryptor, + Encryptor, MultiPartyDecryptor, SampleExtractor, }; use super::*;@@ -363,13 +403,52 @@ ct0 = ct_out;
} } + #[test] + fn batched_fhe_u8s_extract_works() { + set_parameter_set(ParameterSelector::InteractiveLTE2Party); + let mut seed = [0u8; 32]; + thread_rng().fill_bytes(&mut seed); + set_mp_seed(seed); + + let parties = 2; + let cks = (0..parties).map(|_| gen_client_key()).collect_vec(); + + // round 1 + let pk_shares = cks.iter().map(|k| gen_mp_keys_phase1(k)).collect_vec(); + + // collective pk + let pk = aggregate_public_key_shares(&pk_shares); + + let parameters = BoolEvaluator::with_local(|e| e.parameters().clone()); + + let batch_size = parameters.rlwe_n().0 * 3 + 123; + let m = (0..batch_size) + .map(|_| thread_rng().gen::<u8>()) + .collect_vec(); + + let seeded_ct = pk.encrypt(m.as_slice()); + + let m_back = (0..batch_size) + .map(|i| { + let ct = seeded_ct.extract_at(i); + cks[0].aggregate_decryption_shares( + &ct, + &cks.iter() + .map(|k| k.gen_decryption_share(&ct)) + .collect_vec(), + ) + }) + .collect_vec(); + + assert_eq!(m, m_back); + } + mod sp_api { use num_traits::ToPrimitive; - use rand::Rng; use crate::{ bool::impl_bool_frontend::FheBool, pbs::PbsInfo, rgsw::seeded_secret_key_encrypt_rlwe, - Decryptor, SampleExtractor, + Decryptor, }; use super::*;@@ -499,28 +578,6 @@ (rlwes, seed)
}) }) } - } - - #[test] - fn batch_extract_works() { - set_single_party_parameter_sets(SP_TEST_BOOL_PARAMS); - - let (ck, sk) = gen_keys(); - sk.set_server_key(); - - let batch_size = (SP_TEST_BOOL_PARAMS.rlwe_n().0 * 3 + 123); - let m = (0..batch_size) - .map(|_| thread_rng().gen::<u8>()) - .collect_vec(); - - let seeded_ct = ck.encrypt(m.as_slice()); - let ct = seeded_ct.unseed::<Vec<Vec<u64>>>(); - - let m_back = (0..batch_size) - .map(|i| ck.decrypt(&ct.extract_at(i))) - .collect_vec(); - - assert_eq!(m, m_back); } #[test]
@@ -200,11 +200,15 @@ /// Non interactive multi-party specfic encryptor decryptor routines
mod impl_enc_dec { use crate::{ bool::{evaluator::BoolEncoding, keys::NonInteractiveMultiPartyClientKey}, + multi_party::{ + multi_party_aggregate_decryption_shares_and_decrypt, multi_party_decryption_share, + }, pbs::{sample_extract, PbsInfo, WithShoupRepr}, random::{NewWithSeed, RandomFillUniformInModulus}, rgsw::{rlwe_key_switch, seeded_secret_key_encrypt_rlwe}, utils::TryConvertFrom1, - Encryptor, KeySwitchWithId, Matrix, MatrixEntity, MatrixMut, RowEntity, RowMut, + Encryptor, KeySwitchWithId, Matrix, MatrixEntity, MatrixMut, MultiPartyDecryptor, + RowEntity, RowMut, }; use itertools::Itertools; use num_traits::{ToPrimitive, Zero};@@ -347,6 +351,44 @@ .collect_vec();
(rlwes, seed) }) + }) + } + } + + impl<K> MultiPartyDecryptor<bool, <Mat as Matrix>::R> for K + where + K: NonInteractiveMultiPartyClientKey, + <Mat as Matrix>::R: + TryConvertFrom1<[K::Element], CiphertextModulus<<Mat as Matrix>::MatElement>>, + { + type DecryptionShare = <Mat as Matrix>::MatElement; + + fn gen_decryption_share(&self, c: &<Mat as Matrix>::R) -> Self::DecryptionShare { + BoolEvaluator::with_local(|e| { + DefaultSecureRng::with_local_mut(|rng| { + multi_party_decryption_share( + c, + self.sk_rlwe().as_slice(), + e.pbs_info().modop_rlweq(), + rng, + ) + }) + }) + } + + fn aggregate_decryption_shares( + &self, + c: &<Mat as Matrix>::R, + shares: &[Self::DecryptionShare], + ) -> bool { + BoolEvaluator::with_local(|e| { + let noisy_m = multi_party_aggregate_decryption_shares_and_decrypt( + c, + shares, + e.pbs_info().modop_rlweq(), + ); + + e.pbs_info().rlwe_q().decode(noisy_m) }) } }
@@ -1,13 +1,16 @@
use std::fmt::Debug; use itertools::izip; +use num_traits::Zero; use crate::{ - backend::{GetModulus, VectorOps}, + backend::{GetModulus, Modulus, VectorOps}, ntt::Ntt, - random::{RandomFillGaussianInModulus, RandomFillUniformInModulus}, + random::{ + RandomFillGaussianInModulus, RandomFillUniformInModulus, RandomGaussianElementInModulus, + }, utils::TryConvertFrom1, - Matrix, MatrixEntity, MatrixMut, Row, RowEntity, RowMut, + ArithmeticOps, Matrix, MatrixEntity, MatrixMut, Row, RowEntity, RowMut, }; pub(crate) fn public_key_share<@@ -48,6 +51,59 @@ nttop.backward(s.as_mut());
RandomFillGaussianInModulus::random_fill(rng, &q, share_out.as_mut()); modop.elwise_add_mut(share_out.as_mut(), s.as_ref()); // s*e + e +} + +/// Generate decryption share for LWE ciphertext `lwe_ct` with user's secret `s` +pub(crate) fn multi_party_decryption_share< + R: RowMut + RowEntity, + Mod: Modulus<Element = R::Element>, + ModOp: ArithmeticOps<Element = R::Element> + VectorOps<Element = R::Element> + GetModulus<M = Mod>, + Rng: RandomGaussianElementInModulus<R::Element, Mod>, + S, +>( + lwe_ct: &R, + s: &[S], + mod_op: &ModOp, + rng: &mut Rng, +) -> R::Element +where + R: TryConvertFrom1<[S], Mod>, + R::Element: Zero, +{ + assert!(lwe_ct.as_ref().len() == s.len() + 1); + let mut neg_s = R::try_convert_from(s, mod_op.modulus()); + mod_op.elwise_neg_mut(neg_s.as_mut()); + + // share = (\sum -s_i * a_i) + e + let mut share = R::Element::zero(); + izip!(neg_s.as_ref().iter(), lwe_ct.as_ref().iter().skip(1)).for_each(|(si, ai)| { + share = mod_op.add(&share, &mod_op.mul(si, ai)); + }); + + let e = rng.random(mod_op.modulus()); + share = mod_op.add(&share, &e); + + share +} + +/// Aggregate decryption shares for `lwe_ct` and return noisy decryption output +/// `m + e` +pub(crate) fn multi_party_aggregate_decryption_shares_and_decrypt< + R: RowMut + RowEntity, + ModOp: ArithmeticOps<Element = R::Element>, +>( + lwe_ct: &R, + shares: &[R::Element], + mod_op: &ModOp, +) -> R::Element +where + R::Element: Zero, +{ + let mut sum_shares = R::Element::zero(); + shares + .iter() + .for_each(|v| sum_shares = mod_op.add(&sum_shares, v)); + mod_op.add(&lwe_ct.as_ref()[0], &sum_shares) } pub(crate) fn non_interactive_rgsw_ct<
@@ -8,10 +8,11 @@ Decryptor, Encryptor, KeySwitchWithId, Matrix, MatrixEntity, MatrixMut, MultiPartyDecryptor,
RowMut, SampleExtractor, }; -/// Fhe UInt8 type +/// Fhe UInt8 /// -/// - Stores encryptions of bits in little endian (i.e least signficant bit -/// stored at 0th index and most signficant bit stores at 7th index) +/// Note that `Self.data` stores encryptions of bits in little endian (i.e least +/// signficant bit stored at 0th index and most signficant bit stores at 7th +/// index) #[derive(Clone)] pub struct FheUint8<C> { pub(super) data: Vec<C>,@@ -27,7 +28,9 @@ &mut self.data
} } -/// Stored a batch of Fhe Uint8 ciphertext as collection of RLWE ciphertexts +/// Stores a batch of Fhe Uint8 ciphertext as collection of unseeded RLWE +/// ciphertexts always encrypted under the ideal RLWE secret `s` of the MPC +/// protocol /// /// To extract Fhe Uint8 ciphertext at `index` call `self.extract(index)` pub struct BatchedFheUint8<C> {@@ -37,6 +40,68 @@ /// Count of FheUint8s packed in vector of RLWE ciphertexts
count: usize, } +impl<K, C> Encryptor<[u8], BatchedFheUint8<C>> for K +where + K: Encryptor<[bool], Vec<C>>, +{ + /// Encrypt a batch of uint8s packed in vector of RLWE ciphertexts + /// + /// Uint8s can be extracted from `BatchedFheUint8` with `SampleExtractor` + fn encrypt(&self, m: &[u8]) -> BatchedFheUint8<C> { + let bool_m = m + .iter() + .flat_map(|v| { + (0..8) + .into_iter() + .map(|i| ((*v >> i) & 1) == 1) + .collect_vec() + }) + .collect_vec(); + let cts = K::encrypt(&self, &bool_m); + BatchedFheUint8 { + data: cts, + count: m.len(), + } + } +} + +impl<M: MatrixEntity + MatrixMut<MatElement = u64>> From<&SeededBatchedFheUint8<M::R, [u8; 32]>> + for BatchedFheUint8<M> +where + <M as Matrix>::R: RowMut, +{ + /// Unseeds collection of seeded RLWE ciphertext in SeededBatchedFheUint8 + /// and returns as `Self` + fn from(value: &SeededBatchedFheUint8<M::R, [u8; 32]>) -> Self { + BoolEvaluator::with_local(|e| { + let parameters = e.parameters(); + let ring_size = parameters.rlwe_n().0; + let rlwe_q = parameters.rlwe_q(); + + let mut prng = DefaultSecureRng::new_seeded(value.seed); + let rlwes = value + .data + .iter() + .map(|partb| { + let mut rlwe = M::zeros(2, ring_size); + + // sample A + RandomFillUniformInModulus::random_fill(&mut prng, rlwe_q, rlwe.get_row_mut(0)); + + // Copy over B + rlwe.get_row_mut(1).copy_from_slice(partb.as_ref()); + + rlwe + }) + .collect_vec(); + Self { + data: rlwes, + count: value.count, + } + }) + } +} + impl<C, R> SampleExtractor<FheUint8<R>> for BatchedFheUint8<C> where C: SampleExtractor<R>,@@ -82,8 +147,28 @@ .collect_vec()
} } +/// Stores a batch of FheUint8s packed in a collection unseeded RLWE ciphertexts +/// +/// `Self` stores unseeded RLWE ciphertexts encrypted under user's RLWE secret +/// `u_j` and is different from `BatchFheUint8` which stores collection of RLWE +/// ciphertexts under ideal RLWE secret `s` of the (non-interactive/interactive) +/// MPC protocol. +/// +/// To extract FheUint8s from `Self`'s collection of RLWE ciphertexts, first +/// switch `Self` to `BatchFheUint8` with `key_switch(user_id)` where `user_id` +/// is user's id. This key switches collection of RLWE ciphertexts from +/// user's RLWE secret `u_j` to ideal RLWE secret `s` of the MPC protocol. Then +/// proceed to use `SampleExtract` on `BatchFheUint8` (for ex, call +/// `extract_at(0)` to extract FheUint8 stored at index 0) +pub struct NonInteractiveBatchedFheUint8<C> { + /// Vector of RLWE ciphertexts `C` + data: Vec<C>, + /// Count of FheUint8s packed in vector of RLWE ciphertexts + count: usize, +} + impl<M: MatrixEntity + MatrixMut<MatElement = u64>> From<&SeededBatchedFheUint8<M::R, [u8; 32]>> - for BatchedFheUint8<M> + for NonInteractiveBatchedFheUint8<M> where <M as Matrix>::R: RowMut, {@@ -119,6 +204,27 @@ })
} } +impl<C> KeySwitchWithId<BatchedFheUint8<C>> for NonInteractiveBatchedFheUint8<C> +where + C: KeySwitchWithId<C>, +{ + /// Key switch `Self`'s collection of RLWE cihertexts encrypted under user's + /// RLWE secret `u_j` to ideal RLWE secret `s` of the MPC protocol. + /// + /// - user_id: user id of user `j` + fn key_switch(&self, user_id: usize) -> BatchedFheUint8<C> { + let data = self + .data + .iter() + .map(|c| c.key_switch(user_id)) + .collect_vec(); + BatchedFheUint8 { + data, + count: self.count, + } + } +} + pub struct SeededBatchedFheUint8<C, S> { /// Vector of Seeded RLWE ciphertexts `C`. ///@@ -131,21 +237,12 @@ /// Count of FheUint8s packed in vector of RLWE ciphertexts
count: usize, } -impl<C, S> SeededBatchedFheUint8<C, S> { - pub fn unseed<M>(&self) -> BatchedFheUint8<M> - where - BatchedFheUint8<M>: for<'a> From<&'a SeededBatchedFheUint8<C, S>>, - M: Matrix<R = C>, - { - BatchedFheUint8::from(self) - } -} - impl<K, C, S> Encryptor<[u8], SeededBatchedFheUint8<C, S>> for K where K: Encryptor<[bool], (Vec<C>, S)>, { - /// Encrypt a slice of u8s of arbitray length as `SeededBatchedFheUint8` + /// Encrypt a slice of u8s of arbitray length packed into collection of + /// seeded RLWE ciphertexts and return `SeededBatchedFheUint8` fn encrypt(&self, m: &[u8]) -> SeededBatchedFheUint8<C, S> { // convert vector of u8s to vector bools let bool_m = m@@ -161,46 +258,42 @@ }
} } -impl<K, C> Encryptor<[u8], BatchedFheUint8<C>> for K -where - K: Encryptor<[bool], Vec<C>>, -{ - fn encrypt(&self, m: &[u8]) -> BatchedFheUint8<C> { - let bool_m = m - .iter() - .flat_map(|v| { - (0..8) - .into_iter() - .map(|i| ((*v >> i) & 1) == 1) - .collect_vec() - }) - .collect_vec(); - let cts = K::encrypt(&self, &bool_m); - BatchedFheUint8 { - data: cts, - count: m.len(), - } - } -} - -impl<C> KeySwitchWithId<BatchedFheUint8<C>> for BatchedFheUint8<C> -where - C: KeySwitchWithId<C>, -{ - /// Key switching collection of RLWE ciphertexts in `BatchedFheUint8` from - /// user j's RLWE secret u_j to ideal RLWE secret key `s` of the protocol. +impl<C, S> SeededBatchedFheUint8<C, S> { + /// Unseed collection of seeded RLWE ciphertexts of `Self` and returns + /// `NonInteractiveBatchedFheUint8` with collection of unseeded RLWE + /// ciphertexts. + /// + /// In non-interactive MPC setting, RLWE ciphertexts are encrypted under + /// user's RLWE secret `u_j`. The RLWE ciphertexts must be key switched to + /// ideal RLWE secret `s` of the MPC protocol before use. + /// + /// Note that we don't provide `unseed` API from `Self` to + /// `BatchedFheUint8`. This is because: + /// + /// - In non-interactive setting (1) client encrypts private inputs using + /// their secret `u_j` as `SeededBatchedFheUint8` and sends it to the + /// server. (2) Server unseeds `SeededBatchedFheUint8` into + /// `NonInteractiveBatchedFheUint8` indicating that private inputs are + /// still encrypted under user's RLWE secret `u_j`. (3) Server key + /// switches `NonInteractiveBatchedFheUint8` from user's RLWE secret `u_j` + /// to ideal RLWE secret `s` and outputs `BatchedFheUint8`. (4) + /// `BatchedFheUint8` always stores RLWE secret under ideal RLWE secret of + /// the protocol. Hence, it is safe to extract FheUint8s. Server proceeds + /// to extract necessary FheUint8s. + /// + /// - In interactive setting (1) client always encrypts private inputs using + /// public key corresponding to ideal RLWE secret `s` of the protocol and + /// produces `BatchedFheUint8`. (2) Given `BatchedFheUint8` stores + /// collection of RLWE ciphertext under ideal RLWE secret `s`, server can + /// directly extract necessary FheUint8s to use. /// - /// - user_id: user id of user j - fn key_switch(&self, user_id: usize) -> BatchedFheUint8<C> { - let data = self - .data - .iter() - .map(|c| c.key_switch(user_id)) - .collect_vec(); - BatchedFheUint8 { - data, - count: self.count, - } + /// Thus, there's no need to go directly from `Self` to `BatchedFheUint8`. + pub fn unseed<M>(&self) -> NonInteractiveBatchedFheUint8<M> + where + NonInteractiveBatchedFheUint8<M>: for<'a> From<&'a SeededBatchedFheUint8<C, S>>, + M: Matrix<R = C>, + { + NonInteractiveBatchedFheUint8::from(self) } }