use crate::modular::center;
use crate::ntt::NttTables;
use crate::params::Params;
use crate::poly::Poly;
use crate::sampling::{sample_gaussian, sample_ternary, sample_uniform};
use rand::Rng;
#[derive(Clone, Debug)]
pub struct SecretKey {
pub poly: Poly,
}
#[derive(Clone, Debug)]
pub struct PublicKey {
pub a: Poly,
pub b: Poly,
}
#[derive(Clone, Debug)]
pub struct Ciphertext {
pub c0: Poly,
pub c1: Poly,
}
pub fn keygen<R: Rng>(params: &Params, ntt: &NttTables, rng: &mut R) -> (SecretKey, PublicKey) {
let s = sample_ternary(params, rng);
let a = sample_uniform(params, rng);
let e = sample_gaussian(params, rng);
let as_prod = ntt.mul(&a, &s);
let b = e.sub(&as_prod, params);
(SecretKey { poly: s }, PublicKey { a, b })
}
pub fn encrypt<R: Rng>(
params: &Params,
ntt: &NttTables,
pk: &PublicKey,
plaintext: &Poly,
rng: &mut R,
) -> Ciphertext {
let u = sample_ternary(params, rng);
let e1 = sample_gaussian(params, rng);
let e2 = sample_gaussian(params, rng);
let delta = params.q / params.t;
let m_scaled = plaintext.scalar_mul(delta, params);
let bu = ntt.mul(&pk.b, &u);
let c0 = bu.add(&e1, params).add(&m_scaled, params);
let au = ntt.mul(&pk.a, &u);
let c1 = au.add(&e2, params);
Ciphertext { c0, c1 }
}
pub fn decrypt(params: &Params, ntt: &NttTables, sk: &SecretKey, ct: &Ciphertext) -> Poly {
let c1s = ntt.mul(&ct.c1, &sk.poly);
let phase = ct.c0.add(&c1s, params);
let coeffs = phase
.coeffs
.iter()
.map(|&c| {
let centered = center(c, params.q);
let scaled = (centered as f64 * params.t as f64) / params.q as f64;
let rounded = scaled.round() as i64;
let result = rounded.rem_euclid(params.t as i64) as u64;
result
})
.collect();
Poly { coeffs }
}
pub fn add(params: &Params, ct1: &Ciphertext, ct2: &Ciphertext) -> Ciphertext {
Ciphertext {
c0: ct1.c0.add(&ct2.c0, params),
c1: ct1.c1.add(&ct2.c1, params),
}
}
pub fn sub(params: &Params, ct1: &Ciphertext, ct2: &Ciphertext) -> Ciphertext {
Ciphertext {
c0: ct1.c0.sub(&ct2.c0, params),
c1: ct1.c1.sub(&ct2.c1, params),
}
}
pub fn scalar_mul(params: &Params, ct: &Ciphertext, scalar: u64) -> Ciphertext {
Ciphertext {
c0: ct.c0.scalar_mul(scalar, params),
c1: ct.c1.scalar_mul(scalar, params),
}
}
pub fn encrypt_u64<R: Rng>(
params: &Params,
ntt: &NttTables,
pk: &PublicKey,
value: u64,
rng: &mut R,
) -> Ciphertext {
let m = Poly::constant(value % params.t, params.n);
encrypt(params, ntt, pk, &m, rng)
}
pub fn decrypt_u64(params: &Params, ntt: &NttTables, sk: &SecretKey, ct: &Ciphertext) -> u64 {
let m = decrypt(params, ntt, sk, ct);
m.coeffs[0]
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
fn setup() -> (Params, NttTables, StdRng) {
let p = Params::test_small();
let ntt = NttTables::new(&p);
let rng = StdRng::seed_from_u64(42);
(p, ntt, rng)
}
#[test]
fn test_encrypt_decrypt_roundtrip() {
let (p, ntt, mut rng) = setup();
let (sk, pk) = keygen(&p, &ntt, &mut rng);
let m = Poly::from_coeffs(&[5 % p.t, 3 % p.t, 7 % p.t, 1], p.n);
let ct = encrypt(&p, &ntt, &pk, &m, &mut rng);
let m_dec = decrypt(&p, &ntt, &sk, &ct);
for i in 0..4 {
assert_eq!(m.coeffs[i], m_dec.coeffs[i], "mismatch at {i}");
}
}
#[test]
fn test_encrypt_decrypt_u64() {
let (p, ntt, mut rng) = setup();
let (sk, pk) = keygen(&p, &ntt, &mut rng);
for val in 0..p.t {
let ct = encrypt_u64(&p, &ntt, &pk, val, &mut rng);
let dec = decrypt_u64(&p, &ntt, &sk, &ct);
assert_eq!(val, dec, "roundtrip failed for {val}");
}
}
#[test]
fn test_homomorphic_add() {
let (p, ntt, mut rng) = setup();
let (sk, pk) = keygen(&p, &ntt, &mut rng);
let a = 3u64;
let b = 5u64;
let ct_a = encrypt_u64(&p, &ntt, &pk, a, &mut rng);
let ct_b = encrypt_u64(&p, &ntt, &pk, b, &mut rng);
let ct_sum = add(&p, &ct_a, &ct_b);
let dec = decrypt_u64(&p, &ntt, &sk, &ct_sum);
assert_eq!((a + b) % p.t, dec);
}
#[test]
fn test_scalar_mul_ciphertext() {
let (p, ntt, mut rng) = setup();
let (sk, pk) = keygen(&p, &ntt, &mut rng);
let val = 2u64;
let scalar = 3u64;
let ct = encrypt_u64(&p, &ntt, &pk, val, &mut rng);
let ct_scaled = scalar_mul(&p, &ct, scalar);
let dec = decrypt_u64(&p, &ntt, &sk, &ct_scaled);
assert_eq!((val * scalar) % p.t, dec);
}
#[test]
fn test_homomorphic_weighted_sum() {
let (p, ntt, mut rng) = setup();
let (sk, pk) = keygen(&p, &ntt, &mut rng);
let a_val = 2u64;
let b_val = 3u64;
let c_val = 1u64;
let ct_a = encrypt_u64(&p, &ntt, &pk, a_val, &mut rng);
let ct_b = encrypt_u64(&p, &ntt, &pk, b_val, &mut rng);
let ct_c = encrypt_u64(&p, &ntt, &pk, c_val, &mut rng);
let s1 = scalar_mul(&p, &ct_a, 3);
let s2 = scalar_mul(&p, &ct_b, 2);
let s3 = scalar_mul(&p, &ct_c, 1);
let sum = add(&p, &add(&p, &s1, &s2), &s3);
let dec = decrypt_u64(&p, &ntt, &sk, &sum);
let expected = (3 * a_val + 2 * b_val + 1 * c_val) % p.t;
assert_eq!(expected, dec);
}
#[test]
fn test_multiple_seeds() {
let (p, ntt, _) = setup();
for seed in 0..20u64 {
let mut rng = StdRng::seed_from_u64(seed);
let (sk, pk) = keygen(&p, &ntt, &mut rng);
let val = seed % p.t;
let ct = encrypt_u64(&p, &ntt, &pk, val, &mut rng);
let dec = decrypt_u64(&p, &ntt, &sk, &ct);
assert_eq!(val, dec, "failed for seed {seed}");
}
}
}