Skip to main content

plat_core/
rlwe.rs

1//! RLWE (Ring Learning With Errors) encryption primitives.
2//!
3//! Single-key encrypt/decrypt and homomorphic addition in Z_q[X]/(X^N + 1).
4//! This forms the foundation for both single-key FHE and Multi-Key FHE.
5//!
6//! Encryption scheme:
7//!   sk = s (ternary polynomial)
8//!   pk = (a, b = -a*s + e) where a ← uniform, e ← Gaussian
9//!   Encrypt(pk, m): ct = (a*u + e1 + ⌊q/t⌋*m, b*u + e2)  → (c0, c1)
10//!   Decrypt(sk, ct): m = ⌊t/q * (c0 + c1*s)⌉ mod t
11
12use crate::modular::center;
13use crate::ntt::NttTables;
14use crate::params::Params;
15use crate::poly::Poly;
16use crate::sampling::{sample_gaussian, sample_ternary, sample_uniform};
17use rand::Rng;
18
19/// RLWE secret key: a ternary polynomial.
20#[derive(Clone, Debug)]
21pub struct SecretKey {
22    pub poly: Poly,
23}
24
25/// RLWE public key: (a, b) where b = -(a*s) + e.
26#[derive(Clone, Debug)]
27pub struct PublicKey {
28    pub a: Poly,
29    pub b: Poly,
30}
31
32/// RLWE ciphertext: (c0, c1) such that c0 + c1*s ≈ ⌊q/t⌋*m.
33#[derive(Clone, Debug)]
34pub struct Ciphertext {
35    pub c0: Poly,
36    pub c1: Poly,
37}
38
39/// Generate a secret/public key pair.
40pub fn keygen<R: Rng>(params: &Params, ntt: &NttTables, rng: &mut R) -> (SecretKey, PublicKey) {
41    let s = sample_ternary(params, rng);
42    let a = sample_uniform(params, rng);
43    let e = sample_gaussian(params, rng);
44
45    // b = -(a*s) + e = e - a*s
46    let as_prod = ntt.mul(&a, &s);
47    let b = e.sub(&as_prod, params);
48
49    (SecretKey { poly: s }, PublicKey { a, b })
50}
51
52/// Encrypt a plaintext polynomial (coefficients in [0, t)).
53pub fn encrypt<R: Rng>(
54    params: &Params,
55    ntt: &NttTables,
56    pk: &PublicKey,
57    plaintext: &Poly,
58    rng: &mut R,
59) -> Ciphertext {
60    let u = sample_ternary(params, rng);
61    let e1 = sample_gaussian(params, rng);
62    let e2 = sample_gaussian(params, rng);
63
64    // delta = ⌊q/t⌋
65    let delta = params.q / params.t;
66
67    // Scale message: m_scaled = delta * m
68    let m_scaled = plaintext.scalar_mul(delta, params);
69
70    // c0 = b*u + e1 + delta*m
71    let bu = ntt.mul(&pk.b, &u);
72    let c0 = bu.add(&e1, params).add(&m_scaled, params);
73
74    // c1 = a*u + e2
75    let au = ntt.mul(&pk.a, &u);
76    let c1 = au.add(&e2, params);
77
78    Ciphertext { c0, c1 }
79}
80
81/// Decrypt a ciphertext to recover the plaintext polynomial.
82pub fn decrypt(params: &Params, ntt: &NttTables, sk: &SecretKey, ct: &Ciphertext) -> Poly {
83    // phase = c0 + c1 * s ≈ delta * m + small_noise
84    let c1s = ntt.mul(&ct.c1, &sk.poly);
85    let phase = ct.c0.add(&c1s, params);
86
87    // Round: m_i = ⌊t * phase_i / q⌉ mod t
88    let coeffs = phase
89        .coeffs
90        .iter()
91        .map(|&c| {
92            // centered representative
93            let centered = center(c, params.q);
94            // t * centered / q, rounded
95            let scaled = (centered as f64 * params.t as f64) / params.q as f64;
96            let rounded = scaled.round() as i64;
97            let result = rounded.rem_euclid(params.t as i64) as u64;
98            result
99        })
100        .collect();
101
102    Poly { coeffs }
103}
104
105/// Homomorphic addition of two ciphertexts.
106pub fn add(params: &Params, ct1: &Ciphertext, ct2: &Ciphertext) -> Ciphertext {
107    Ciphertext {
108        c0: ct1.c0.add(&ct2.c0, params),
109        c1: ct1.c1.add(&ct2.c1, params),
110    }
111}
112
113/// Homomorphic subtraction: ct1 - ct2.
114pub fn sub(params: &Params, ct1: &Ciphertext, ct2: &Ciphertext) -> Ciphertext {
115    Ciphertext {
116        c0: ct1.c0.sub(&ct2.c0, params),
117        c1: ct1.c1.sub(&ct2.c1, params),
118    }
119}
120
121/// Multiply ciphertext by a plaintext scalar (no noise growth from second operand).
122pub fn scalar_mul(params: &Params, ct: &Ciphertext, scalar: u64) -> Ciphertext {
123    Ciphertext {
124        c0: ct.c0.scalar_mul(scalar, params),
125        c1: ct.c1.scalar_mul(scalar, params),
126    }
127}
128
129/// Encrypt a single u64 value (placed in the constant coefficient).
130pub fn encrypt_u64<R: Rng>(
131    params: &Params,
132    ntt: &NttTables,
133    pk: &PublicKey,
134    value: u64,
135    rng: &mut R,
136) -> Ciphertext {
137    let m = Poly::constant(value % params.t, params.n);
138    encrypt(params, ntt, pk, &m, rng)
139}
140
141/// Decrypt to a single u64 value (from the constant coefficient).
142pub fn decrypt_u64(params: &Params, ntt: &NttTables, sk: &SecretKey, ct: &Ciphertext) -> u64 {
143    let m = decrypt(params, ntt, sk, ct);
144    m.coeffs[0]
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use rand::SeedableRng;
151    use rand::rngs::StdRng;
152
153    fn setup() -> (Params, NttTables, StdRng) {
154        // Use test_small (N=256, q=12289, t=257) for adequate noise budget.
155        // test_tiny (q=769, t=17) has too little headroom for scalar multiplication.
156        let p = Params::test_small();
157        let ntt = NttTables::new(&p);
158        let rng = StdRng::seed_from_u64(42);
159        (p, ntt, rng)
160    }
161
162    #[test]
163    fn test_encrypt_decrypt_roundtrip() {
164        let (p, ntt, mut rng) = setup();
165        let (sk, pk) = keygen(&p, &ntt, &mut rng);
166
167        // Values must be < p.t
168        let m = Poly::from_coeffs(&[5 % p.t, 3 % p.t, 7 % p.t, 1], p.n);
169        let ct = encrypt(&p, &ntt, &pk, &m, &mut rng);
170        let m_dec = decrypt(&p, &ntt, &sk, &ct);
171
172        for i in 0..4 {
173            assert_eq!(m.coeffs[i], m_dec.coeffs[i], "mismatch at {i}");
174        }
175    }
176
177    #[test]
178    fn test_encrypt_decrypt_u64() {
179        let (p, ntt, mut rng) = setup();
180        let (sk, pk) = keygen(&p, &ntt, &mut rng);
181
182        for val in 0..p.t {
183            let ct = encrypt_u64(&p, &ntt, &pk, val, &mut rng);
184            let dec = decrypt_u64(&p, &ntt, &sk, &ct);
185            assert_eq!(val, dec, "roundtrip failed for {val}");
186        }
187    }
188
189    #[test]
190    fn test_homomorphic_add() {
191        let (p, ntt, mut rng) = setup();
192        let (sk, pk) = keygen(&p, &ntt, &mut rng);
193
194        let a = 3u64;
195        let b = 5u64;
196        let ct_a = encrypt_u64(&p, &ntt, &pk, a, &mut rng);
197        let ct_b = encrypt_u64(&p, &ntt, &pk, b, &mut rng);
198        let ct_sum = add(&p, &ct_a, &ct_b);
199        let dec = decrypt_u64(&p, &ntt, &sk, &ct_sum);
200        assert_eq!((a + b) % p.t, dec);
201    }
202
203    #[test]
204    fn test_scalar_mul_ciphertext() {
205        let (p, ntt, mut rng) = setup();
206        let (sk, pk) = keygen(&p, &ntt, &mut rng);
207
208        let val = 2u64;
209        let scalar = 3u64;
210        let ct = encrypt_u64(&p, &ntt, &pk, val, &mut rng);
211        let ct_scaled = scalar_mul(&p, &ct, scalar);
212        let dec = decrypt_u64(&p, &ntt, &sk, &ct_scaled);
213        assert_eq!((val * scalar) % p.t, dec);
214    }
215
216    #[test]
217    fn test_homomorphic_weighted_sum() {
218        let (p, ntt, mut rng) = setup();
219        let (sk, pk) = keygen(&p, &ntt, &mut rng);
220
221        let a_val = 2u64;
222        let b_val = 3u64;
223        let c_val = 1u64;
224
225        let ct_a = encrypt_u64(&p, &ntt, &pk, a_val, &mut rng);
226        let ct_b = encrypt_u64(&p, &ntt, &pk, b_val, &mut rng);
227        let ct_c = encrypt_u64(&p, &ntt, &pk, c_val, &mut rng);
228
229        // 3*a + 2*b + 1*c = 6 + 6 + 1 = 13
230        let s1 = scalar_mul(&p, &ct_a, 3);
231        let s2 = scalar_mul(&p, &ct_b, 2);
232        let s3 = scalar_mul(&p, &ct_c, 1);
233
234        let sum = add(&p, &add(&p, &s1, &s2), &s3);
235        let dec = decrypt_u64(&p, &ntt, &sk, &sum);
236        let expected = (3 * a_val + 2 * b_val + 1 * c_val) % p.t;
237        assert_eq!(expected, dec);
238    }
239
240    #[test]
241    fn test_multiple_seeds() {
242        let (p, ntt, _) = setup();
243
244        for seed in 0..20u64 {
245            let mut rng = StdRng::seed_from_u64(seed);
246            let (sk, pk) = keygen(&p, &ntt, &mut rng);
247            let val = seed % p.t;
248            let ct = encrypt_u64(&p, &ntt, &pk, val, &mut rng);
249            let dec = decrypt_u64(&p, &ntt, &sk, &ct);
250            assert_eq!(val, dec, "failed for seed {seed}");
251        }
252    }
253}