1#![forbid(unsafe_code)]
21#![allow(missing_docs)] pub mod keys;
24pub mod shamir;
25
26use aes_gcm::aead::AeadInOut;
27use aes_gcm::aead::inout::InOutBuf;
28use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
29use p256::elliptic_curve::PrimeField;
30use p256::elliptic_curve::rand_core;
31use p256::elliptic_curve::rand_core::Rng;
32use p256::elliptic_curve::sec1::Sec1Point;
33use p256::elliptic_curve::sec1::{FromSec1Point, ToSec1Point};
34use p256::elliptic_curve::subtle::CtOption;
35use p256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar};
36use serde::{Deserialize, Serialize};
37use sha2::{Digest as _, Sha256};
38
39pub use keys::generate_keypair;
40pub use shamir::{Share, recover_secret, split_secret};
41
42pub const ALGORITHM: &str = "ECIES-P256-threshold";
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PublicKey {
48 pub bytes: Vec<u8>,
50}
51
52impl PublicKey {
53 pub fn from_affine(point: AffinePoint) -> Self {
55 Self {
56 bytes: point.to_sec1_point(false).as_bytes().to_vec(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct DecryptionShare {
64 pub party_index: u32,
66 pub bytes: Vec<u8>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct EncryptedBlob {
73 pub ephemeral_public: Vec<u8>,
75 pub ciphertext: Vec<u8>,
77 pub nonce: Vec<u8>,
79 pub tag: Vec<u8>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PartialDecryption {
86 pub party_index: u32,
88 pub bytes: Vec<u8>,
90}
91
92#[derive(Debug, thiserror::Error)]
94pub enum EciesError {
95 #[error("threshold not met: have {have}, need {need}")]
97 ThresholdNotMet {
98 have: usize,
100 need: u32,
102 },
103 #[error("SEC1 decode failed: {0}")]
105 Sec1Decode(String),
106 #[error("duplicate party index: {0}")]
108 DuplicateParty(u32),
109 #[error("invalid scalar: {0}")]
111 InvalidScalar(String),
112 #[error("AEAD failure: {0}")]
114 Aead(String),
115}
116
117fn decode_point(bytes: &[u8]) -> Result<ProjectivePoint, EciesError> {
118 let ep = Sec1Point::<p256::NistP256>::from_bytes(bytes)
119 .map_err(|e| EciesError::Sec1Decode(format!("encoded point: {e}")))?;
120 let ct_opt = AffinePoint::from_sec1_point(&ep);
121 let affine = Option::<AffinePoint>::from(ct_opt)
122 .ok_or_else(|| EciesError::Sec1Decode("point at infinity".into()))?;
123 Ok(ProjectivePoint::from(affine))
124}
125
126fn encode_point(point: &ProjectivePoint) -> Vec<u8> {
127 point.to_affine().to_sec1_point(false).as_bytes().to_vec()
128}
129
130fn decode_scalar(bytes: &[u8]) -> Result<Scalar, EciesError> {
131 if bytes.len() != 32 {
132 return Err(EciesError::InvalidScalar(format!(
133 "expected 32 bytes, got {}",
134 bytes.len()
135 )));
136 }
137 let mut arr = [0u8; 32];
138 arr.copy_from_slice(bytes);
139 let fb = FieldBytes::from(arr);
140 let ct: CtOption<Scalar> = Scalar::from_repr(fb);
141 Option::<Scalar>::from(ct).ok_or_else(|| EciesError::InvalidScalar("out of range".into()))
142}
143
144fn slice_to_array<const N: usize>(bytes: &[u8]) -> Result<[u8; N], EciesError> {
145 bytes
146 .try_into()
147 .map_err(|_| EciesError::Aead(format!("expected {N} bytes, got {}", bytes.len())))
148}
149
150fn x_coordinate(point: &ProjectivePoint) -> [u8; 32] {
151 let ep = point.to_affine().to_sec1_point(false);
152 let bytes = ep.as_bytes();
153 if bytes.len() == 65 {
154 let mut arr = [0u8; 32];
155 arr.copy_from_slice(&bytes[1..33]);
156 arr
157 } else {
158 [0u8; 32]
159 }
160}
161
162fn derive_aead_key(shared: &[u8; 32]) -> [u8; 32] {
163 use sha2::Digest;
164 let mut h = sha2::Sha256::new();
167 h.update(b"confium-ecies-p256-v1");
168 h.update(shared);
169 let out = h.finalize();
170 let mut key = [0u8; 32];
171 key.copy_from_slice(&out);
172 key
173}
174
175pub fn encrypt(recipient: &PublicKey, plaintext: &[u8]) -> Result<EncryptedBlob, EciesError> {
177 let recipient_pt = decode_point(&recipient.bytes)?;
178
179 let r = loop {
181 let mut buf = [0u8; 32];
182 rand_core::UnwrapErr(getrandom::SysRng).fill_bytes(&mut buf);
183 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(buf))) {
184 if s != Scalar::ZERO {
185 break s;
186 }
187 }
188 };
189
190 let r_point = ProjectivePoint::GENERATOR * r;
192 let ephemeral_public = encode_point(&r_point);
193
194 let shared_point = recipient_pt * r;
196 let shared = x_coordinate(&shared_point);
197
198 let key = derive_aead_key(&shared);
200 let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| EciesError::Aead(e.to_string()))?;
201
202 let mut nonce_bytes = [0u8; 12];
204 rand_core::UnwrapErr(getrandom::SysRng).fill_bytes(&mut nonce_bytes);
205 let nonce = Nonce::from(nonce_bytes);
206
207 let mut buffer = plaintext.to_vec();
209 let tag = cipher
210 .encrypt_inout_detached(&nonce, b"", InOutBuf::from(buffer.as_mut_slice()))
211 .map_err(|e| EciesError::Aead(e.to_string()))?;
212
213 Ok(EncryptedBlob {
214 ephemeral_public,
215 ciphertext: buffer,
216 nonce: nonce_bytes.to_vec(),
217 tag: tag.to_vec(),
218 })
219}
220
221pub fn partial_decrypt(
223 share: &DecryptionShare,
224 blob: &EncryptedBlob,
225) -> Result<PartialDecryption, EciesError> {
226 let s = decode_scalar(&share.bytes)?;
227 let r_point = decode_point(&blob.ephemeral_public)?;
228 let partial = r_point * s;
229 Ok(PartialDecryption {
230 party_index: share.party_index,
231 bytes: encode_point(&partial),
232 })
233}
234
235pub fn aggregate_partials(
240 partials: &[PartialDecryption],
241 threshold: u32,
242 blob: &EncryptedBlob,
243) -> Result<Vec<u8>, EciesError> {
244 if (partials.len() as u32) < threshold {
245 return Err(EciesError::ThresholdNotMet {
246 have: partials.len(),
247 need: threshold,
248 });
249 }
250
251 let mut seen = std::collections::HashSet::new();
252 for p in partials {
253 if !seen.insert(p.party_index) {
254 return Err(EciesError::DuplicateParty(p.party_index));
255 }
256 }
257
258 let mut combined = ProjectivePoint::IDENTITY;
260 for p_i in partials {
261 let x_i = party_to_scalar(p_i.party_index);
262 let mut numerator = Scalar::ONE;
263 let mut denominator = Scalar::ONE;
264 for p_j in partials {
265 if p_j.party_index == p_i.party_index {
266 continue;
267 }
268 let x_j = party_to_scalar(p_j.party_index);
269 numerator *= negate(&x_j);
270 denominator *= x_i.sub(&x_j);
271 }
272 let denom_inv = invert(&denominator);
273 let lagrange = numerator * denom_inv;
274
275 let partial_point = decode_point(&p_i.bytes)?;
276 let weighted = partial_point * lagrange;
277 combined = combined.add(&weighted);
278 }
279
280 let shared = x_coordinate(&combined);
282 let key = derive_aead_key(&shared);
283 let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| EciesError::Aead(e.to_string()))?;
284
285 let nonce = Nonce::from(slice_to_array::<12>(&blob.nonce)?);
287 let tag = aes_gcm::Tag::from(slice_to_array::<16>(&blob.tag)?);
288 let mut buffer = blob.ciphertext.clone();
289 cipher
290 .decrypt_inout_detached(&nonce, b"", InOutBuf::from(buffer.as_mut_slice()), &tag)
291 .map_err(|e| EciesError::Aead(format!("decrypt: {e}")))?;
292
293 Ok(buffer)
294}
295
296fn negate(s: &Scalar) -> Scalar {
297 Scalar::ZERO.sub(s)
298}
299
300fn invert(s: &Scalar) -> Scalar {
301 let ct: CtOption<Scalar> = s.invert();
304 Option::<Scalar>::from(ct).unwrap_or(Scalar::ZERO)
305}
306
307fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
310 loop {
311 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
312 return s;
313 }
314 let mut h = Sha256::new();
315 h.update(b"confium-scalar-reduce-v1");
316 h.update(bytes);
317 bytes = h.finalize().into();
318 }
319}
320
321fn party_to_scalar(v: u32) -> Scalar {
322 let mut arr = [0u8; 32];
323 arr[28..32].copy_from_slice(&v.to_be_bytes());
324 reduce_to_scalar(arr)
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn encrypt_then_decrypt_round_trip() {
333 let keypair = generate_keypair();
335 let pk = PublicKey::from_affine(keypair.public_key);
336 let shares = split_secret(&keypair.secret_scalar, 2, 3);
337
338 let plaintext = b"hello, threshold ECIES world";
339 let blob = encrypt(&pk, plaintext).unwrap();
340
341 let decryption_shares: Vec<DecryptionShare> = shares
343 .iter()
344 .map(|s| DecryptionShare {
345 party_index: s.x,
346 bytes: {
347 let fb: FieldBytes = s.y.to_bytes();
348 let arr: [u8; 32] = fb.into();
349 arr.to_vec()
350 },
351 })
352 .collect();
353 let partials: Vec<PartialDecryption> = decryption_shares
354 .iter()
355 .take(2)
356 .map(|ds| partial_decrypt(ds, &blob).unwrap())
357 .collect();
358
359 let recovered = aggregate_partials(&partials, 2, &blob).unwrap();
360 assert_eq!(recovered.as_slice(), plaintext);
361 }
362
363 #[test]
364 fn different_share_subsets_recover_same_plaintext() {
365 let keypair = generate_keypair();
366 let pk = PublicKey::from_affine(keypair.public_key);
367 let shares = split_secret(&keypair.secret_scalar, 3, 5);
368
369 let plaintext = b"consistency check across share subsets";
370 let blob = encrypt(&pk, plaintext).unwrap();
371
372 let decryption_shares: Vec<DecryptionShare> = shares
373 .iter()
374 .map(|s| DecryptionShare {
375 party_index: s.x,
376 bytes: {
377 let fb: FieldBytes = s.y.to_bytes();
378 let arr: [u8; 32] = fb.into();
379 arr.to_vec()
380 },
381 })
382 .collect();
383
384 let partials_a: Vec<PartialDecryption> = decryption_shares[0..3]
386 .iter()
387 .map(|ds| partial_decrypt(ds, &blob).unwrap())
388 .collect();
389 let recovered_a = aggregate_partials(&partials_a, 3, &blob).unwrap();
390
391 let partials_b: Vec<PartialDecryption> = decryption_shares[2..5]
393 .iter()
394 .map(|ds| partial_decrypt(ds, &blob).unwrap())
395 .collect();
396 let recovered_b = aggregate_partials(&partials_b, 3, &blob).unwrap();
397
398 assert_eq!(recovered_a.as_slice(), plaintext);
399 assert_eq!(recovered_b.as_slice(), plaintext);
400 }
401
402 #[test]
403 fn threshold_not_met_fails() {
404 let keypair = generate_keypair();
405 let pk = PublicKey::from_affine(keypair.public_key);
406 let shares = split_secret(&keypair.secret_scalar, 3, 5);
407 let blob = encrypt(&pk, b"x").unwrap();
408 let decryption_shares: Vec<DecryptionShare> = shares
409 .iter()
410 .map(|s| DecryptionShare {
411 party_index: s.x,
412 bytes: {
413 let fb: FieldBytes = s.y.to_bytes();
414 let arr: [u8; 32] = fb.into();
415 arr.to_vec()
416 },
417 })
418 .collect();
419 let partials: Vec<PartialDecryption> = decryption_shares[0..2]
420 .iter()
421 .map(|ds| partial_decrypt(ds, &blob).unwrap())
422 .collect();
423 let result = aggregate_partials(&partials, 3, &blob);
424 assert!(matches!(result, Err(EciesError::ThresholdNotMet { .. })));
425 }
426
427 #[test]
428 fn wrong_subset_fails_to_decrypt() {
429 let keypair = generate_keypair();
431 let pk = PublicKey::from_affine(keypair.public_key);
432 let _shares = split_secret(&keypair.secret_scalar, 2, 3);
433 let blob = encrypt(&pk, b"secret").unwrap();
434
435 let other = generate_keypair();
437 let other_shares = split_secret(&other.secret_scalar, 2, 3);
438 let wrong_partials: Vec<PartialDecryption> = other_shares[0..2]
439 .iter()
440 .map(|s| {
441 partial_decrypt(
442 &DecryptionShare {
443 party_index: s.x,
444 bytes: {
445 let fb: FieldBytes = s.y.to_bytes();
446 let arr: [u8; 32] = fb.into();
447 arr.to_vec()
448 },
449 },
450 &blob,
451 )
452 .unwrap()
453 })
454 .collect();
455 let result = aggregate_partials(&wrong_partials, 2, &blob);
456 assert!(matches!(result, Err(EciesError::Aead(_))));
457 }
458}