1#![forbid(unsafe_code)]
13#![allow(missing_docs)] pub mod keys;
16pub mod shamir;
17
18use p256::elliptic_curve::PrimeField;
19use p256::elliptic_curve::rand_core;
20use p256::elliptic_curve::sec1::ToSec1Point;
21use p256::elliptic_curve::subtle::CtOption;
22use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
23use serde::{Deserialize, Serialize};
24use sha2::{Digest as _, Sha256};
25
26pub use keys::generate_keypair;
27pub use shamir::{Share, recover_secret, split_secret};
28
29pub const ALGORITHM: &str = "ElGamal-P256-threshold";
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PublicKey {
35 pub bytes: Vec<u8>,
37}
38
39impl PublicKey {
40 pub fn from_affine(point: AffinePoint) -> Self {
42 Self {
43 bytes: point.to_sec1_point(false).as_bytes().to_vec(),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct DecryptionShare {
52 pub party_index: u32,
54 pub bytes: Vec<u8>,
56}
57
58impl Drop for DecryptionShare {
59 fn drop(&mut self) {
60 use zeroize::Zeroize;
61 self.bytes.zeroize();
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Ciphertext {
68 pub c1: Vec<u8>,
70 pub c2: Vec<u8>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct PartialDecryption {
77 pub party_index: u32,
79 pub bytes: Vec<u8>,
81}
82
83#[derive(Debug, thiserror::Error)]
85pub enum ElGamalError {
86 #[error("threshold not met: have {have}, need {need}")]
88 ThresholdNotMet {
89 have: usize,
91 need: u32,
93 },
94 #[error("SEC1 decode failed: {0}")]
96 Sec1Decode(String),
97 #[error("duplicate party index: {0}")]
99 DuplicateParty(u32),
100 #[error("invalid scalar: {0}")]
102 InvalidScalar(String),
103}
104
105fn decode_point(bytes: &[u8]) -> Result<ProjectivePoint, ElGamalError> {
106 use p256::elliptic_curve::sec1::FromSec1Point;
107 let ep = p256::elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
108 .map_err(|e| ElGamalError::Sec1Decode(format!("encoded point: {e}")))?;
109 let ct_opt = AffinePoint::from_sec1_point(&ep);
110 let affine = Option::<AffinePoint>::from(ct_opt)
111 .ok_or_else(|| ElGamalError::Sec1Decode("point at infinity".into()))?;
112 Ok(ProjectivePoint::from(affine))
113}
114
115fn encode_point(point: &ProjectivePoint) -> Vec<u8> {
116 point.to_affine().to_sec1_point(false).as_bytes().to_vec()
117}
118
119fn decode_scalar(bytes: &[u8]) -> Result<Scalar, ElGamalError> {
120 if bytes.len() != 32 {
121 return Err(ElGamalError::InvalidScalar(format!(
122 "expected 32 bytes, got {}",
123 bytes.len()
124 )));
125 }
126 let mut arr = [0u8; 32];
127 arr.copy_from_slice(bytes);
128 let fb = FieldBytes::from(arr);
129 let ct: CtOption<Scalar> = Scalar::from_repr(fb);
130 Option::<Scalar>::from(ct).ok_or_else(|| ElGamalError::InvalidScalar("out of range".into()))
131}
132
133pub fn encapsulate(
144 recipient_public_key: &PublicKey,
145) -> Result<(Ciphertext, Vec<u8>), ElGamalError> {
146 use p256::elliptic_curve::rand_core::Rng;
147
148 let recipient_pt = decode_point(&recipient_public_key.bytes)?;
149
150 let r = loop {
152 let mut buf = [0u8; 32];
153 rand_core::UnwrapErr(getrandom::SysRng).fill_bytes(&mut buf);
154 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(buf))) {
155 if s != Scalar::ZERO {
156 break s;
157 }
158 }
159 };
160
161 let c1 = ProjectivePoint::GENERATOR * r;
162 let c2 = recipient_pt * r;
163
164 let c1_bytes = encode_point(&c1);
165 let c2_bytes = encode_point(&c2);
166 let shared_secret = x_coordinate(&c2);
167
168 Ok((
169 Ciphertext {
170 c1: c1_bytes,
171 c2: c2_bytes,
172 },
173 shared_secret,
174 ))
175}
176
177pub fn partial_decrypt(
179 share: &DecryptionShare,
180 ciphertext: &Ciphertext,
181) -> Result<PartialDecryption, ElGamalError> {
182 let s = decode_scalar(&share.bytes)?;
183 let c1 = decode_point(&ciphertext.c1)?;
184 let partial = c1 * s;
185 Ok(PartialDecryption {
186 party_index: share.party_index,
187 bytes: encode_point(&partial),
188 })
189}
190
191pub fn aggregate_partials(
197 partials: &[PartialDecryption],
198 threshold: u32,
199 _ciphertext: &Ciphertext,
200) -> Result<Vec<u8>, ElGamalError> {
201 if (partials.len() as u32) < threshold {
202 return Err(ElGamalError::ThresholdNotMet {
203 have: partials.len(),
204 need: threshold,
205 });
206 }
207
208 let mut seen = std::collections::HashSet::new();
209 for p in partials {
210 if !seen.insert(p.party_index) {
211 return Err(ElGamalError::DuplicateParty(p.party_index));
212 }
213 }
214
215 let mut combined = ProjectivePoint::IDENTITY;
217 for p_i in partials {
218 let x_i = party_to_scalar(p_i.party_index);
219 let mut numerator = Scalar::ONE;
220 let mut denominator = Scalar::ONE;
221 for p_j in partials {
222 if p_j.party_index == p_i.party_index {
223 continue;
224 }
225 let x_j = party_to_scalar(p_j.party_index);
226 numerator *= negate(&x_j);
227 denominator *= x_i.sub(&x_j);
228 }
229 let denom_inv = invert(&denominator);
230 let lagrange = numerator * denom_inv;
231
232 let partial_point = decode_point(&p_i.bytes)?;
233 let weighted = partial_point * lagrange;
234 combined = combined.add(&weighted);
235 }
236
237 Ok(x_coordinate(&combined))
238}
239
240fn x_coordinate(point: &ProjectivePoint) -> Vec<u8> {
241 let affine = point.to_affine();
242 let ep = affine.to_sec1_point(false);
243 let bytes = ep.as_bytes();
244 if bytes.len() == 65 {
245 bytes[1..33].to_vec()
246 } else {
247 bytes.to_vec()
248 }
249}
250
251fn negate(s: &Scalar) -> Scalar {
252 Scalar::ZERO.sub(s)
253}
254
255fn invert(s: &Scalar) -> Scalar {
256 let ct: CtOption<Scalar> = s.invert();
259 Option::<Scalar>::from(ct).unwrap_or(Scalar::ZERO)
260}
261
262fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
265 loop {
266 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
267 return s;
268 }
269 let mut h = Sha256::new();
270 h.update(b"confium-scalar-reduce-v1");
271 h.update(bytes);
272 bytes = h.finalize().into();
273 }
274}
275
276fn party_to_scalar(v: u32) -> Scalar {
277 let mut arr = [0u8; 32];
278 arr[28..32].copy_from_slice(&v.to_be_bytes());
279 reduce_to_scalar(arr)
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn encapsulate_then_aggregate_round_trip() {
288 let keypair = generate_keypair();
290 let pk = PublicKey::from_affine(keypair.public_key);
291 let shares = split_secret(&keypair.secret_scalar, 2, 3);
292
293 let (ciphertext, shared_secret) = encapsulate(&pk).unwrap();
295 assert_eq!(shared_secret.len(), 32);
296
297 let decryption_shares: Vec<DecryptionShare> = shares
299 .iter()
300 .map(|s| DecryptionShare {
301 party_index: s.x,
302 bytes: {
303 let fb: FieldBytes = s.y.to_bytes();
304 let arr: [u8; 32] = fb.into();
305 arr.to_vec()
306 },
307 })
308 .collect();
309 let partials: Vec<PartialDecryption> = decryption_shares
310 .iter()
311 .take(2)
312 .map(|ds| partial_decrypt(ds, &ciphertext).unwrap())
313 .collect();
314
315 let recovered = aggregate_partials(&partials, 2, &ciphertext).unwrap();
317 assert_eq!(recovered.len(), 32);
318 assert_eq!(recovered, shared_secret);
320 }
321
322 #[test]
323 fn aggregate_below_threshold_fails() {
324 let keypair = generate_keypair();
325 let pk = PublicKey::from_affine(keypair.public_key);
326 let shares = split_secret(&keypair.secret_scalar, 3, 5);
327 let (ciphertext, _) = encapsulate(&pk).unwrap();
328 let decryption_shares: Vec<DecryptionShare> = shares
329 .iter()
330 .map(|s| DecryptionShare {
331 party_index: s.x,
332 bytes: {
333 let fb: FieldBytes = s.y.to_bytes();
334 let arr: [u8; 32] = fb.into();
335 arr.to_vec()
336 },
337 })
338 .collect();
339 let partials: Vec<PartialDecryption> = decryption_shares
340 .iter()
341 .take(2)
342 .map(|ds| partial_decrypt(ds, &ciphertext).unwrap())
343 .collect();
344 let result = aggregate_partials(&partials, 3, &ciphertext);
345 assert!(matches!(result, Err(ElGamalError::ThresholdNotMet { .. })));
346 }
347
348 #[test]
349 fn different_share_subsets_recover_same_secret() {
350 let keypair = generate_keypair();
351 let pk = PublicKey::from_affine(keypair.public_key);
352 let shares = split_secret(&keypair.secret_scalar, 3, 5);
353 let (ciphertext, shared_secret) = encapsulate(&pk).unwrap();
354
355 let decryption_shares: Vec<DecryptionShare> = shares
356 .iter()
357 .map(|s| DecryptionShare {
358 party_index: s.x,
359 bytes: {
360 let fb: FieldBytes = s.y.to_bytes();
361 let arr: [u8; 32] = fb.into();
362 arr.to_vec()
363 },
364 })
365 .collect();
366
367 let partials_a: Vec<PartialDecryption> = decryption_shares[0..3]
369 .iter()
370 .map(|ds| partial_decrypt(ds, &ciphertext).unwrap())
371 .collect();
372 let recovered_a = aggregate_partials(&partials_a, 3, &ciphertext).unwrap();
373
374 let partials_b: Vec<PartialDecryption> = decryption_shares[2..5]
376 .iter()
377 .map(|ds| partial_decrypt(ds, &ciphertext).unwrap())
378 .collect();
379 let recovered_b = aggregate_partials(&partials_b, 3, &ciphertext).unwrap();
380
381 assert_eq!(recovered_a, shared_secret);
382 assert_eq!(recovered_b, shared_secret);
383 }
384}
385
386#[cfg(test)]
387mod proptests {
388 use super::*;
389 use proptest::prelude::*;
390
391 proptest! {
394 #[test]
395 fn elgamal_roundtrip_any_threshold(t in 2u32..=5, n in 5u32..=8) {
396 let kp = generate_keypair();
397 let pub_key = PublicKey::from_affine(kp.public_key);
398 let shares = split_secret(&kp.secret_scalar, t, n);
399 let decryption_shares: Vec<DecryptionShare> = shares
400 .iter()
401 .map(|s| DecryptionShare {
402 party_index: s.x,
403 bytes: {
404 let fb: FieldBytes = s.y.to_bytes();
405 let arr: [u8; 32] = fb.into();
406 arr.to_vec()
407 },
408 })
409 .collect();
410
411 let (ciphertext, shared_secret) = encapsulate(&pub_key)?;
412
413 let partials: Vec<PartialDecryption> = decryption_shares[..t as usize]
414 .iter()
415 .map(|ds| partial_decrypt(ds, &ciphertext).unwrap())
416 .collect();
417 let recovered = aggregate_partials(&partials, t, &ciphertext)?;
418
419 prop_assert_eq!(recovered, shared_secret);
420 }
421 }
422
423 proptest! {
425 #[test]
426 fn elgamal_below_threshold_fails(t in 2u32..=4, n in 5u32..=8) {
427 let kp = generate_keypair();
428 let pub_key = PublicKey::from_affine(kp.public_key);
429 let shares = split_secret(&kp.secret_scalar, t, n);
430 let decryption_shares: Vec<DecryptionShare> = shares
431 .iter()
432 .map(|s| DecryptionShare {
433 party_index: s.x,
434 bytes: {
435 let fb: FieldBytes = s.y.to_bytes();
436 let arr: [u8; 32] = fb.into();
437 arr.to_vec()
438 },
439 })
440 .collect();
441
442 let (ciphertext, _) = encapsulate(&pub_key)?;
443
444 let partials: Vec<PartialDecryption> = decryption_shares[..(t - 1) as usize]
445 .iter()
446 .map(|ds| partial_decrypt(ds, &ciphertext).unwrap())
447 .collect();
448 let result = aggregate_partials(&partials, t, &ciphertext);
449 prop_assert!(result.is_err(), "below-threshold should fail");
450 }
451 }
452}