Skip to main content

helpers_codec/common/
pke.rs

1// MIT License
2//
3// Copyright (c) 2026 Raja Lehtihet & Wael El Oraiby
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23//! Example RLWE-style public-key encryption API.
24//!
25//! This module is intentionally educational and not hardened for production use.
26
27use rand_core::CryptoRng;
28
29use super::codec::{
30    CodecError, decode_message_scaled_bits_example, encode_message_scaled_bits_example,
31};
32use super::sampling::{SamplingError, sample_cbd_poly_example, sample_uniform_poly_example};
33use nc_polynomial::{PolynomialError, RingContext, RingElem};
34
35/// Public key for the example RLWE-style encryption API.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ExamplePublicKey {
38    /// Public random polynomial `a`.
39    pub a: RingElem,
40    /// Public polynomial `b = a*s + e`.
41    pub b: RingElem,
42}
43
44/// Secret key for the example RLWE-style encryption API.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct ExampleSecretKey {
47    /// Secret small-noise polynomial.
48    pub s: RingElem,
49}
50
51/// Ciphertext for the example RLWE-style encryption API.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ExampleCiphertext {
54    /// First component `u = a*r + e1`.
55    pub u: RingElem,
56    /// Second component `v = b*r + e2 + m`.
57    pub v: RingElem,
58}
59
60/// Errors returned by the example RLWE-style API.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ExamplePkeError {
63    /// Sampling helper failed.
64    Sampling(SamplingError),
65    /// Codec helper failed.
66    Codec(CodecError),
67    /// Ring arithmetic failed.
68    Polynomial(PolynomialError),
69    /// Input objects do not belong to the provided context.
70    ParameterMismatch,
71}
72
73impl core::fmt::Display for ExamplePkeError {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        match self {
76            Self::Sampling(err) => write!(f, "sampling failed: {err}"),
77            Self::Codec(err) => write!(f, "codec failed: {err}"),
78            Self::Polynomial(err) => write!(f, "polynomial arithmetic failed: {err}"),
79            Self::ParameterMismatch => write!(f, "parameters/context mismatch"),
80        }
81    }
82}
83
84impl std::error::Error for ExamplePkeError {}
85
86impl From<SamplingError> for ExamplePkeError {
87    fn from(value: SamplingError) -> Self {
88        Self::Sampling(value)
89    }
90}
91
92impl From<CodecError> for ExamplePkeError {
93    fn from(value: CodecError) -> Self {
94        Self::Codec(value)
95    }
96}
97
98impl From<PolynomialError> for ExamplePkeError {
99    fn from(value: PolynomialError) -> Self {
100        Self::Polynomial(value)
101    }
102}
103
104/// Generates an example keypair using centered-binomial secret/noise samples.
105pub fn keygen_example<R: CryptoRng>(
106    ctx: &RingContext,
107    noise_eta: u8,
108    rng: &mut R,
109) -> Result<(ExamplePublicKey, ExampleSecretKey), ExamplePkeError> {
110    // Public random polynomial.
111    let a = sample_uniform_poly_example(ctx, rng)?;
112    // Secret small-noise polynomial.
113    let s = sample_cbd_poly_example(ctx, noise_eta, rng)?;
114    // Public error polynomial.
115    let e = sample_cbd_poly_example(ctx, noise_eta, rng)?;
116
117    // RLWE public relation: b = a*s + e.
118    let b = a.mul(&s)?.add(&e)?;
119
120    Ok((ExamplePublicKey { a, b }, ExampleSecretKey { s }))
121}
122
123/// Encrypts a byte message using the example key.
124///
125/// The message is encoded using scaled bits from [`encode_message_scaled_bits_example`].
126pub fn encrypt_example<R: CryptoRng>(
127    ctx: &RingContext,
128    public_key: &ExamplePublicKey,
129    message: &[u8],
130    noise_eta: u8,
131    rng: &mut R,
132) -> Result<ExampleCiphertext, ExamplePkeError> {
133    // Ensure all key elements come from the same validated parameter set.
134    ensure_element_matches_context(ctx, &public_key.a)?;
135    ensure_element_matches_context(ctx, &public_key.b)?;
136
137    // Encode message bits into ring element form.
138    let m = encode_message_scaled_bits_example(ctx, message)?;
139    // Fresh encryption randomness + masking errors.
140    let r = sample_cbd_poly_example(ctx, noise_eta, rng)?;
141    let e1 = sample_cbd_poly_example(ctx, noise_eta, rng)?;
142    let e2 = sample_cbd_poly_example(ctx, noise_eta, rng)?;
143
144    // RLWE encryption equations:
145    //   u = a*r + e1
146    //   v = b*r + e2 + m
147    let u = public_key.a.mul(&r)?.add(&e1)?;
148    let v = public_key.b.mul(&r)?.add(&e2)?.add(&m)?;
149
150    Ok(ExampleCiphertext { u, v })
151}
152
153/// Decrypts an example ciphertext and decodes the requested number of output bytes.
154pub fn decrypt_example(
155    ctx: &RingContext,
156    secret_key: &ExampleSecretKey,
157    ciphertext: &ExampleCiphertext,
158    output_len_bytes: usize,
159) -> Result<Vec<u8>, ExamplePkeError> {
160    // Context checks prevent cross-parameter misuse.
161    ensure_element_matches_context(ctx, &secret_key.s)?;
162    ensure_element_matches_context(ctx, &ciphertext.u)?;
163    ensure_element_matches_context(ctx, &ciphertext.v)?;
164
165    // Recover encoded message:
166    //   v - s*u = (a*s + e)r + e2 + m - s(a*r + e1)
167    //           = m + (e*r + e2 - s*e1)
168    // Small noise lets threshold decoder recover bits.
169    let recovered = ciphertext.v.sub(&secret_key.s.mul(&ciphertext.u)?)?;
170    Ok(decode_message_scaled_bits_example(
171        &recovered,
172        output_len_bytes,
173    )?)
174}
175
176fn ensure_element_matches_context(
177    ctx: &RingContext,
178    element: &RingElem,
179) -> Result<(), ExamplePkeError> {
180    // Structural parameter equality check.
181    if element.params() != ctx.params() {
182        return Err(ExamplePkeError::ParameterMismatch);
183    }
184    Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189    use super::{
190        ExampleCiphertext, ExamplePkeError, ExamplePublicKey, decrypt_example, encrypt_example,
191        keygen_example,
192    };
193    use core::convert::Infallible;
194    use nc_polynomial::RingContext;
195    use rand_core::{Rng, TryCryptoRng, TryRng};
196
197    #[derive(Debug, Clone)]
198    struct TestRng {
199        state: u64,
200    }
201
202    impl TestRng {
203        fn new(seed: u64) -> Self {
204            Self { state: seed }
205        }
206
207        fn step(&mut self) -> u64 {
208            let mut x = self.state;
209            x ^= x >> 12;
210            x ^= x << 25;
211            x ^= x >> 27;
212            self.state = x;
213            x.wrapping_mul(0x2545_F491_4F6C_DD1D)
214        }
215    }
216
217    impl TryRng for TestRng {
218        type Error = Infallible;
219
220        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
221            Ok(self.step() as u32)
222        }
223
224        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
225            Ok(self.step())
226        }
227
228        fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
229            let mut offset = 0;
230            while offset < dest.len() {
231                let word = self.step().to_le_bytes();
232                let chunk = (dest.len() - offset).min(8);
233                dest[offset..offset + chunk].copy_from_slice(&word[..chunk]);
234                offset += chunk;
235            }
236            Ok(())
237        }
238    }
239
240    impl TryCryptoRng for TestRng {}
241
242    fn ctx(max_degree: usize) -> RingContext {
243        let mut modulus_poly = vec![0_u64; max_degree + 1];
244        modulus_poly[0] = 1;
245        modulus_poly[max_degree] = 1;
246
247        RingContext::from_parts(max_degree, 998_244_353, &modulus_poly, 3)
248            .expect("context should build")
249    }
250
251    #[test]
252    fn example_pke_known_answer_vector() {
253        // Deterministic seeds make vectors stable and reproducible.
254        let ctx = ctx(16);
255
256        let mut key_rng = TestRng::new(0x1234_5678_90AB_CDEF);
257        let (pk, sk) = keygen_example(&ctx, 2, &mut key_rng).expect("keygen should work");
258
259        let message = [0xA5_u8, 0x5A];
260        let mut enc_rng = TestRng::new(0x0FED_CBA9_8765_4321);
261        let ct =
262            encrypt_example(&ctx, &pk, &message, 2, &mut enc_rng).expect("encrypt should work");
263
264        assert_eq!(
265            sk.s.trimmed_coefficients(),
266            vec![
267                998244352, 2, 0, 998244352, 0, 2, 998244352, 1, 1, 0, 998244352, 998244352, 2,
268                998244352, 1,
269            ]
270        );
271        assert_eq!(
272            pk.a.trimmed_coefficients(),
273            vec![
274                981506489, 745670232, 465336855, 4228489, 817594600, 449616339, 214586882,
275                863302342, 412683612, 739770493, 484384582, 958972188, 465458099, 901548526,
276                136656154, 185069671
277            ]
278        );
279        assert_eq!(
280            pk.b.trimmed_coefficients(),
281            vec![
282                523016218, 151678120, 905400193, 336053334, 886938703, 345066676, 653029107,
283                919882161, 719988960, 738092331, 49398785, 531018501, 299386522, 26450153,
284                202169127, 149348579
285            ]
286        );
287        assert_eq!(
288            ct.u.trimmed_coefficients(),
289            vec![
290                185825251, 992594928, 222735870, 419735116, 492318984, 871565634, 299285265,
291                894961651, 551797064, 802414206, 845696604, 713894635, 456686131, 629006526,
292                242614853, 521523065
293            ]
294        );
295        assert_eq!(
296            ct.v.trimmed_coefficients(),
297            vec![
298                342654269, 292514575, 651371554, 799848083, 605114155, 22609066, 790897921,
299                525265555, 989917947, 531922498, 781997577, 952513402, 961072975, 455278710,
300                776307615, 91714186
301            ]
302        );
303
304        let decrypted =
305            decrypt_example(&ctx, &sk, &ct, message.len()).expect("decrypt should work");
306        // Reference vector is only valid if decrypt round-trip is correct.
307        assert_eq!(decrypted, message);
308    }
309
310    #[test]
311    fn example_pke_encrypt_decrypt_round_trip_many_messages() {
312        let ctx = ctx(32);
313
314        // Vary seeds and message lengths to cover different bit layouts.
315        for seed in 0_u64..64 {
316            let mut key_rng = TestRng::new(0xAA55_AA55_0000_0000 ^ seed);
317            let (pk, sk) = keygen_example(&ctx, 2, &mut key_rng).expect("keygen should work");
318
319            let msg_len = (seed as usize) % 5;
320            let mut msg_rng = TestRng::new(0xC0DE_CAFE_0000_0000 ^ seed);
321            let mut message = vec![0_u8; msg_len];
322            msg_rng.fill_bytes(&mut message);
323
324            let mut enc_rng = TestRng::new(0xDEAD_BEEF_0000_0000 ^ seed);
325            let ct =
326                encrypt_example(&ctx, &pk, &message, 2, &mut enc_rng).expect("encrypt should work");
327            let recovered =
328                decrypt_example(&ctx, &sk, &ct, message.len()).expect("decrypt should work");
329
330            assert_eq!(recovered, message, "seed {seed}");
331        }
332    }
333
334    #[test]
335    fn example_pke_rejects_parameter_mismatch_inputs() {
336        let ctx_a = ctx(16);
337        let ctx_b = ctx(32);
338
339        let mut rng = TestRng::new(101);
340        let (pk_a, sk_a) = keygen_example(&ctx_a, 2, &mut rng).expect("keygen should work");
341
342        let bad_pk = ExamplePublicKey {
343            // Mismatched context in `a`.
344            a: ctx_b.one_element(),
345            b: pk_a.b.clone(),
346        };
347        let mut enc_rng = TestRng::new(102);
348        let err = encrypt_example(&ctx_a, &bad_pk, b"a", 2, &mut enc_rng)
349            .expect_err("expected mismatch error");
350        assert_eq!(err, ExamplePkeError::ParameterMismatch);
351
352        let mut good_enc_rng = TestRng::new(103);
353        let good_ct =
354            encrypt_example(&ctx_a, &pk_a, b"a", 2, &mut good_enc_rng).expect("encrypt works");
355        let bad_ct = ExampleCiphertext {
356            // Mismatched context in `u`.
357            u: ctx_b.one_element(),
358            v: good_ct.v,
359        };
360        let err = decrypt_example(&ctx_a, &sk_a, &bad_ct, 1).expect_err("expected mismatch");
361        assert_eq!(err, ExamplePkeError::ParameterMismatch);
362    }
363
364    #[test]
365    fn example_pke_property_like_round_trips_many_seeds() {
366        let ctx = ctx(32);
367
368        // Property-like deterministic sweep:
369        // different eta values, message lengths, and seed mixes.
370        for seed in 0_u64..128 {
371            let mut key_rng = TestRng::new(0x1111_2222_3333_4444 ^ seed.rotate_left(5));
372            let (pk, sk) = keygen_example(&ctx, 1 + (seed % 2) as u8, &mut key_rng)
373                .expect("keygen should work");
374
375            let msg_len = ((seed * 7) as usize) % 5;
376            let mut msg_rng = TestRng::new(0x5555_6666_7777_8888 ^ seed.rotate_left(11));
377            let mut message = vec![0_u8; msg_len];
378            msg_rng.fill_bytes(&mut message);
379
380            let mut enc_rng = TestRng::new(0x9999_AAAA_BBBB_CCCC ^ seed.rotate_left(17));
381            let ciphertext =
382                encrypt_example(&ctx, &pk, &message, 1 + (seed % 2) as u8, &mut enc_rng)
383                    .expect("encrypt should work");
384            let recovered = decrypt_example(&ctx, &sk, &ciphertext, message.len())
385                .expect("decrypt should work");
386            assert_eq!(recovered, message, "seed {seed}");
387        }
388    }
389}