//! Implementation of BFV https://eprint.iacr.org/2012/144.pdf #![allow(non_snake_case)] #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(clippy::upper_case_acronyms)] #![allow(dead_code)] // TMP use anyhow::{anyhow, Result}; use rand::Rng; use rand_distr::{Normal, Uniform}; use std::ops; use arithmetic::{Rq, Zq, R}; // error deviation for the Gaussian(Normal) distribution // sigma=3.2 from: https://eprint.iacr.org/2022/162.pdf page 5 const ERR_SIGMA: f64 = 3.2; // const ERR_SIGMA: f64 = 0.0; // TODO WIP #[derive(Clone, Debug)] pub struct SecretKey(Rq); #[derive(Clone, Debug)] pub struct PublicKey(Rq, Rq); /// Relinearization key #[derive(Clone, Debug)] pub struct RLK(Rq, Rq); // impl RLK { // // const P: u64 = PQ / Q; // // // const PQ: u64 = P * Q; // } // RLWE ciphertext #[derive(Clone, Debug)] pub struct RLWE(Rq, Rq); impl RLWE { fn add(lhs: Self, rhs: Self) -> Self { RLWE::(lhs.0 + rhs.0, lhs.1 + rhs.1) } pub fn remodule(&self) -> RLWE { let x = self.0.remodule::

(); let y = self.1.remodule::

(); RLWE::(x, y) } fn tensor(a: &Self, b: &Self) -> (Rq, Rq, Rq) { // expand Q->PQ // TODO rm // get the coefficients in Z, ie. interpret a,b \in R (instead of R_q) let a0: R = a.0.to_r(); let a1: R = a.1.to_r(); let b0: R = b.0.to_r(); let b1: R = b.1.to_r(); // tensor (\in R) use arithmetic::ring::naive_mul; let c0: Vec = naive_mul(&a0, &b0); let c1_l: Vec = naive_mul(&a0, &b1); let c1_r = naive_mul(&a1, &b0); let c1: Vec = itertools::zip_eq(c1_l, c1_r).map(|(l, r)| l + r).collect(); let c2: Vec = naive_mul(&a1, &b1); // scale down, module Q, so result is \in R_q let c0: Rq = arithmetic::ring::mul_div_round::(c0, T, Q); let c1: Rq = arithmetic::ring::mul_div_round::(c1, T, Q); let c2: Rq = arithmetic::ring::mul_div_round::(c2, T, Q); (c0, c1, c2) } fn tensor_DBG( a: &Self, b: &Self, ) -> (Rq, Rq, Rq) { // iacr 2021-204: // expand Q->PQ // let a: RLWE = a.remodule::(); // let b: RLWE = b.remodule::(); // tensor let c0: Rq = a.0 * b.0; // NTT mul let c1: Rq = a.0 * b.1 + a.1 * b.0; // NTT mul let c2: Rq = a.1 * b.1; // NTT mul // scale down let c0: Rq = c0.mul_div_round(T, Q); let c1: Rq = c1.mul_div_round(T, Q); let c2: Rq = c2.mul_div_round(T, Q); // expand^-1 PQ->Q // let c0: Rq = c0.remodule::(); // let c1: Rq = c1.remodule::(); // let c2: Rq = c2.remodule::(); (c0, c1, c2) } /// ciphertext multiplication fn mul(rlk: &RLK, a: &Self, b: &Self) -> Self { let (c0, c1, c2) = Self::tensor::(a, b); BFV::::relinearize_204::(&rlk, &c0, &c1, &c2) } } // naive mul in the ring Rq, reusing the ring::naive_mul and then applying mod(X^N +1) fn tmp_naive_mul(a: Rq, b: Rq) -> Rq { Rq::::from_vec_i64(arithmetic::ring::naive_mul(&a.to_r(), &b.to_r())) } impl ops::Add> for RLWE { type Output = Self; fn add(self, rhs: Self) -> Self { Self::add(self, rhs) } } impl ops::Add<&Rq> for &RLWE { type Output = RLWE; fn add(self, rhs: &Rq) -> Self::Output { BFV::::add_const(self, rhs) } } impl ops::Mul<&Rq> for &RLWE { type Output = RLWE; fn mul(self, rhs: &Rq) -> Self::Output { BFV::::mul_const(&self, rhs) } } pub struct BFV {} impl BFV { const DELTA: u64 = Q / T; /// generate a new key pair (privK, pubK) pub fn new_key(mut rng: impl Rng) -> Result<(SecretKey, PublicKey)> { // WIP: review probabilities // let Xi_key = Uniform::new(-1_f64, 1_f64); let Xi_key = Uniform::new(0_u64, 2_u64); // let Xi_key = Uniform::new(0_u64, 2_u64); // use rand::distributions::Bernoulli; // let Xi_key = Bernoulli::new(0.5)?; let Xi_err = Normal::new(0_f64, ERR_SIGMA)?; // secret key let s = Rq::::rand_u64(&mut rng, Xi_key)?; #[cfg(test)] // sanity check assert!(s.infinity_norm() <= 1, "{:?}", s.coeffs()); // pk = (-a * s + e, a) let a = Rq::::rand_u64(&mut rng, Uniform::new(0_u64, Q))?; let e = Rq::::rand_f64(&mut rng, Xi_err)?; let pk: PublicKey = PublicKey((&(-a) * &s) + e, a.clone()); Ok((SecretKey(s), pk)) } pub fn encrypt(mut rng: impl Rng, pk: &PublicKey, m: &Rq) -> Result> { // let Xi_key = Uniform::new(-1_f64, 1_f64); // let Xi_key = Uniform::new(0_f64, 2_f64); let Xi_key = Uniform::new(0_u64, 2_u64); let Xi_err = Normal::new(0_f64, ERR_SIGMA)?; let u = Rq::::rand_u64(&mut rng, Xi_key)?; let e_1 = Rq::::rand_f64_abs(&mut rng, Xi_err)?; let e_2 = Rq::::rand_f64_abs(&mut rng, Xi_err)?; // println!("{:?}", &e_1.coeffs()); // println!("{:?}", &e_2.coeffs()); #[cfg(test)] // sanity check assert!(u.infinity_norm() <= 1, "{:?}", u.coeffs()); // migrate m's coeffs to the bigger modulus Q (from T) let m = m.remodule::(); #[cfg(test)] { // sanity check // TODO rm let m_remod_naive = Rq::::from_vec_u64(m.coeffs().iter().map(|m_i| m_i.0).collect()); assert_eq!(m_remod_naive, m); } // let c0 = &pk.0 * &u + e_1 + m * Self::DELTA; let c0 = &pk.0 * &u + e_1 + m.mul_div_round(Q, T); // TODO use DELTA? let c1 = &pk.1 * &u + e_2; // let c0 = tmp_naive_mul(pk.0, u) + e_1 + m * Self::DELTA; // let c0 = tmp_naive_mul(pk.0, u) + e_1 + m.mul_div_round(Q, T); // let c1 = tmp_naive_mul(pk.1, u) // // &pk.1 * &u // + e_2; Ok(RLWE::(c0, c1)) } pub fn decrypt(sk: &SecretKey, c: &RLWE) -> Rq { let cs = c.0 + c.1 * sk.0; // done in mod q // let c1s = tmp_naive_mul(c.1, sk.0); // // let c1s = arithmetic::ring::naive_mul(&c.1.to_r(), &sk.0.to_r()); // TODO rm // // let c1s = Rq::::from_vec_i64(c1s); // let cs = c.0 + c1s; // let r: Vec = cs // .coeffs() // .iter() // .map(|e| ((T as f64 * e.0 as f64) / Q as f64).round() as u64) // .collect(); // Rq::::from_vec_u64(r) let r: Rq = cs.mul_div_round(T, Q); r.remodule::() } fn add_const(c: &RLWE, m: &Rq) -> RLWE { // assuming T to Zq let m = m.remodule::(); RLWE::(c.0 + m * Self::DELTA, c.1) } fn mul_const(c: &RLWE, m: &Rq) -> RLWE { // assuming T to Zq let m = m.remodule::(); RLWE::(c.0 * m * Self::DELTA, c.1) } fn rlk_key(mut rng: impl Rng, s: &SecretKey) -> Result> { let Xi_err = Normal::new(0_f64, ERR_SIGMA)?; // TODO review Xi' instead of Xi let s = s.0.remodule::(); let a = Rq::::rand_u64(&mut rng, Uniform::new(0_u64, PQ))?; let e = Rq::::rand_f64(&mut rng, Xi_err)?; // let rlk_1: Rq = (&(-a) * &s) - e + (s * s) * P; let P = PQ / Q; // let rlk: RLK = RLK::((&(-a) * &s) - e + (s * s) * P, a.clone()); let rlk: RLK = RLK::( -(tmp_naive_mul(a, s) + e) + tmp_naive_mul(s, s) * P, a.clone(), ); // let rlk: RLK = RLK::(-(&a * &s + e) + (s * s) * P, a.clone()); // let pq = P * Q; // let a = Rq::::rand_u64(&mut rng, Uniform::new(0_u64, pq))?; // let e = Rq::::rand_f64(&mut rng, Xi_err)?; // // let rlk_0: Rq = (&(-a) * &s) - e + (s * s) * P; // let rlk_0 = rlk_0.remodule::<>(); // let rlk: RLK = RLK(rlk_0, a); Ok(rlk) } fn relinearize( rlk: &RLK, c0: &Rq, c1: &Rq, c2: &Rq, ) -> RLWE { let P = PQ / Q; // let c2 = c2.remodule::(); // let c2 = c2.to_r(); let c2rlk0: Vec = (c2.to_r() * rlk.0.to_r()) .coeffs() .iter() .map(|e| (*e as f64 / P as f64).round()) .collect(); let c2rlk1: Vec = (c2.to_r() * rlk.1.to_r()) .coeffs() .iter() .map(|e| (*e as f64 / P as f64).round()) .collect(); let r0 = Rq::::from_vec_f64(c2rlk0); let r1 = Rq::::from_vec_f64(c2rlk1); let res = RLWE::(c0 + &r0, c1 + &r1); res } fn relinearize_204( rlk: &RLK, c0: &Rq, c1: &Rq, c2: &Rq, ) -> RLWE { let P = PQ / Q; // let c2 = c2.remodule::(); // let c2 = c2.to_r(); // let c2rlk0: Vec = (c2.remodule::() * rlk.0) use arithmetic::ring::naive_mul; let c2rlk0: Vec = naive_mul(&c2.to_r(), &rlk.0.to_r()); // .coeffs() // .iter() // .map(|e| (*e as f64 / P as f64).round()) // .collect(); let c2rlk1: Vec = naive_mul(&c2.to_r(), &rlk.1.to_r()); // .coeffs() // .iter() // .map(|e| (*e as f64 / P as f64).round()) // .collect(); // let r0: Rq = arithmetic::ring::mul_div_round::(c2rlk0, 1, P); let r1: Rq = arithmetic::ring::mul_div_round::(c2rlk1, 1, P); // let r0 = Rq::::from_vec_f64(c2rlk0); // let r1 = Rq::::from_vec_f64(c2rlk1); let res = RLWE::(c0 + &r0, c1 + &r1); res } } #[cfg(test)] mod tests { use anyhow::Result; use rand::distributions::Uniform; use super::*; #[test] fn test_encrypt_decrypt() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; const N: usize = 32; const T: u64 = 4; // plaintext modulus type S = BFV; let mut rng = rand::thread_rng(); for _ in 0..1000 { let (sk, pk) = S::new_key(&mut rng)?; let msg_dist = Uniform::new(0_u64, T); let m = Rq::::rand_u64(&mut rng, msg_dist)?; let c = S::encrypt(&mut rng, &pk, &m)?; let m_recovered = S::decrypt(&sk, &c); assert_eq!(m, m_recovered); } Ok(()) } #[test] fn test_addition() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; // const N: usize = 32; const N: usize = 4; const T: u64 = 4; // plaintext modulus type S = BFV; let mut rng = rand::thread_rng(); let (sk, pk) = S::new_key(&mut rng)?; let msg_dist = Uniform::new(0_u64, T); let m1 = Rq::::rand_u64(&mut rng, msg_dist)?; let m2 = Rq::::rand_u64(&mut rng, msg_dist)?; let c1 = S::encrypt(&mut rng, &pk, &m1)?; let c2 = S::encrypt(&mut rng, &pk, &m2)?; let c3 = c1 + c2; let m3_recovered = S::decrypt(&sk, &c3); assert_eq!(m1 + m2, m3_recovered); Ok(()) } #[test] fn test_constant_add() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; const N: usize = 32; const T: u64 = 4; // plaintext modulus type S = BFV; let mut rng = rand::thread_rng(); let (sk, pk) = S::new_key(&mut rng)?; let msg_dist = Uniform::new(0_u64, T); let m1 = Rq::::rand_u64(&mut rng, msg_dist)?; let m2_const = Rq::::rand_u64(&mut rng, msg_dist)?; let c1 = S::encrypt(&mut rng, &pk, &m1)?; let c3_add = &c1 + &m2_const; // let c3_mul = &c1 * &m2_const; let m3_add_recovered = S::decrypt(&sk, &c3_add); // let m3_mul_recovered = S::decrypt(&sk, &c3_mul); assert_eq!(m1 + m2_const, m3_add_recovered); // // let mut mul_res = naive_poly_mul::(&m1.coeffs().to_vec(), &m2_const.coeffs().to_vec()); // arithmetic::ring::modulus::(&mut mul_res); // dbg!(&mul_res); // let mul_res_2 = // naive_poly_mul_2::(&m1.coeffs().to_vec(), &m2_const.coeffs().to_vec()); // assert_eq!(mul_res, mul_res_2); // let mul_res = PR::::from_vec(mul_res); // assert_eq!(mul_res.coeffs(), m3_mul_recovered.coeffs()); Ok(()) } #[test] fn test_tensor() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; // q prime, and 2^q + 1 shape const N: usize = 8; const T: u64 = 4; // plaintext modulus // const P: u64 = Q; const P: u64 = Q * Q; // const P: u64 = 2_u64.pow(13) * Q + 1; const PQ: u64 = P * Q; let mut rng = rand::thread_rng(); let msg_dist = Uniform::new(0_u64, T); for _ in 0..10_000 { let m1 = Rq::::rand_u64(&mut rng, msg_dist)?; let m2 = Rq::::rand_u64(&mut rng, msg_dist)?; test_tensor_opt::(&mut rng, m1, m2)?; } Ok(()) } fn test_tensor_opt( mut rng: impl Rng, m1: Rq, m2: Rq, ) -> Result<()> { let (sk, pk) = BFV::::new_key(&mut rng)?; let c1 = BFV::::encrypt(&mut rng, &pk, &m1)?; let c2 = BFV::::encrypt(&mut rng, &pk, &m2)?; let (c_a, c_b, c_c) = RLWE::::tensor::(&c1, &c2); // decrypt non-relinearized mul result let m3: Rq = c_a + c_b * sk.0 + c_c * sk.0 * sk.0; let m3: Rq = m3.mul_div_round(T, Q); let m3 = m3.remodule::(); let naive = (m1.to_r() * m2.to_r()).to_rq::(); assert_eq!( m3.coeffs().to_vec(), naive.coeffs().to_vec(), "\n\nfor testing:\nlet m1 = Rq::::from_vec_u64(vec!{:?});\nlet m2 = Rq::::from_vec_u64(vec!{:?});\n", m1.coeffs(), m2.coeffs() ); if m3.coeffs().to_vec() != naive.coeffs().to_vec() { return Err(anyhow!("not eq")); } Ok(()) } fn test_tensor_opt_DBG( mut rng: impl Rng, m1: Rq, m2: Rq, ) -> Result<()> { let (sk, pk) = BFV::::new_key(&mut rng)?; let c1 = BFV::::encrypt(&mut rng, &pk, &m1)?; let c2 = BFV::::encrypt(&mut rng, &pk, &m2)?; let (c_a, c_b, c_c) = RLWE::::tensor::(&c1, &c2); // decrypt non-relinearized mul result let m3: Rq = c_a + c_b * sk.0 + c_c * sk.0 * sk.0; dbg!(m3); let m3: Rq = m3.mul_div_round(T, Q); dbg!(m3); let m3 = m3.remodule::(); dbg!(m3); // let naive = (m1.to_r() * m2.to_r()).to_rq::(); // let naive = m1.remodule::() * m2.remodule::(); let naive = (m1.remodule::() * m2.remodule::()).remodule::(); dbg!(naive); assert_eq!( m3.coeffs().to_vec(), naive.coeffs().to_vec(), "\n\nfor testing:\nlet m1 = Rq::::from_vec_u64(vec!{:?});\nlet m2 = Rq::::from_vec_u64(vec!{:?});\n", m1.coeffs(), m2.coeffs() ); // if m3.coeffs().to_vec() != naive.coeffs().to_vec() { // return Err(anyhow!("not eq")); // } Ok(()) } #[test] fn test_mul_relin() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; const N: usize = 32; const T: u64 = 4; // plaintext modulus type S = BFV; const P: u64 = Q * Q; const PQ: u64 = P * Q; let mut rng = rand::thread_rng(); let msg_dist = Uniform::new(0_u64, T); for _ in 0..100 { let m1 = Rq::::rand_u64(&mut rng, msg_dist)?; let m2 = Rq::::rand_u64(&mut rng, msg_dist)?; test_mul_relin_opt::(&mut rng, m1, m2)?; } Ok(()) } fn test_mul_relin_opt( mut rng: impl Rng, m1: Rq, m2: Rq, ) -> Result<()> { let (sk, pk) = BFV::::new_key(&mut rng)?; let rlk = BFV::::rlk_key::(&mut rng, &sk)?; let c1 = BFV::::encrypt(&mut rng, &pk, &m1)?; let c2 = BFV::::encrypt(&mut rng, &pk, &m2)?; let c3 = RLWE::::mul::(&rlk, &c1, &c2); let m3 = BFV::::decrypt(&sk, &c3); let naive = (m1.to_r() * m2.to_r()).to_rq::(); assert_eq!(m3.coeffs().to_vec(), naive.coeffs().to_vec(), "\n\nfor testing:\nlet m1 = Rq::::from_vec_u64(vec!{:?});\nlet m2 = Rq::::from_vec_u64(vec!{:?});\n", m1.coeffs(), m2.coeffs() ); Ok(()) } #[test] fn test_naive_mul() -> Result<()> { const Q: u64 = 2u64.pow(16) + 1; // prime, and 2^q + 1 shape const N: usize = 4; const T: u64 = 4; // plaintext modulus let mut rng = rand::thread_rng(); let msg_dist = Uniform::new(0_u64, T); // for _ in 0..10_000 { for _ in 0..2 { println!("---"); // let a = Rq::::rand_u64(&mut rng, msg_dist)?; // let b = Rq::::rand_u64(&mut rng, msg_dist)?; // let a = Rq::::from_vec_u64(vec![Q - 1, Q - 2, Q - 3, Q - 3]); // let b = Rq::::from_vec_u64(vec![Q - 3, Q - 3, Q - 2, Q - 1]); let a = Rq::::rand_u64(&mut rng, msg_dist)?; let b = Rq::::rand_u64(&mut rng, msg_dist)?; dbg!(&a); dbg!(&b); let (_, pk) = BFV::::new_key(&mut rng)?; let ciph1 = BFV::::encrypt(&mut rng, &pk, &a)?; let ciph2 = BFV::::encrypt(&mut rng, &pk, &b)?; let a = ciph1.0; let b = ciph2.0; dbg!(&a); dbg!(&b); let c0: Vec = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); let c0 = Rq::::from_vec_i64(c0); let c1 = tmp_naive_mul(a, b); // naive mul let c2: Rq = a * b; // NTT mul println!("{:?}", c0.coeffs()); println!("{:?}", c1.coeffs()); println!("{:?}", c2.coeffs()); assert_eq!(c0, c2); assert_eq!(c1, c2); // scale by Delta=Q/T let a = a.mul_div_round(Q, T); let b = b.mul_div_round(Q, T); dbg!(&a); dbg!(&b); let c0: Vec = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); let c0 = Rq::::from_vec_i64(c0); let c1 = tmp_naive_mul(a, b); // naive mul let c2: Rq = a * b; // NTT mul println!("{:?}", c0.coeffs()); println!("{:?}", c1.coeffs()); println!("{:?}", c2.coeffs()); assert_eq!(c0, c2); assert_eq!(c1, c2); let c0 = c0.mul_div_round(T, Q); let c1 = c1.mul_div_round(T, Q); let c2 = c2.mul_div_round(T, Q); println!("{:?}", c0.coeffs()); println!("{:?}", c1.coeffs()); println!("{:?}", c2.coeffs()); assert_eq!(c0, c2); assert_eq!(c1, c2); /* // now same as before, but multiplying by T/Q let c0: Vec = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); let c0: Vec = c0 .iter() .map(|e| ((T as f64 * *e as f64) / Q as f64).round()) .collect(); let c0 = Rq::::from_vec_f64(c0); dbg!(&c0.coeffs()); let a = a.mul_div_round(T, Q); let b = b.mul_div_round(T, Q); println!("a{:?}", a.coeffs()); println!("b{:?}", b.coeffs()); let c4: Vec = arithmetic::ring::naive_mul(&a.to_r(), &b.to_r()); let c4 = Rq::::from_vec_i64(c4); let c4 = c4.mul_div_round(T, Q); let c1 = tmp_naive_mul(a, b); // naive mul let c1 = c1.mul_div_round(T, Q); let c2 = a * b; // NTT mul let c2 = c2.mul_div_round(T, Q); println!("{:?}", c0.coeffs()); println!("{:?}", c1.coeffs()); println!("{:?}", c2.coeffs()); println!("{:?}", c4.coeffs()); assert_eq!(c0, c2); assert_eq!(c4, c2); assert_eq!(c1, c2); */ } Ok(()) } }