confium_tc_cmp20/
share.rs1use elliptic_curve::sec1::ToSec1Point;
4use p256::{AffinePoint, FieldBytes, NonZeroScalar, Scalar};
5use zeroize::Zeroize;
6
7use crate::error::{Cmp20ErrorCode, Result, scheme_error};
8
9const SHARE_MAGIC: [u8; 4] = *b"CMP2";
10const SHARE_VERSION: u8 = 1;
11pub const SHARE_BYTES: usize = 4 + 1 + 32 + 33 + 1;
13
14#[derive(Clone)]
16pub struct Cmp20Share {
17 pub x_i: NonZeroScalar,
19 pub public_key: AffinePoint,
21 pub party_idx: u32,
23}
24
25impl std::fmt::Debug for Cmp20Share {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("Cmp20Share")
28 .field("x_i", &"<redacted>")
29 .field("party_idx", &self.party_idx)
30 .finish_non_exhaustive()
31 }
32}
33
34impl Drop for Cmp20Share {
35 fn drop(&mut self) {
36 let mut bytes = self.x_i.to_bytes();
37 bytes.zeroize();
38 }
39}
40
41impl Cmp20Share {
42 pub fn to_bytes(&self) -> Vec<u8> {
43 let mut out = Vec::with_capacity(SHARE_BYTES);
44 out.extend_from_slice(&SHARE_MAGIC);
45 out.push(SHARE_VERSION);
46 out.extend_from_slice(&self.x_i.to_bytes());
47 out.extend_from_slice(self.public_key.to_sec1_point(true).as_bytes());
48 out.push(self.party_idx as u8);
49 out
50 }
51
52 pub fn from_bytes(data: &[u8]) -> Result<Self> {
53 if data.len() != SHARE_BYTES {
54 return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
55 }
56 if data[0..4] != SHARE_MAGIC {
57 return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
58 }
59 if data[4] != SHARE_VERSION {
60 return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
61 }
62 let mut x_i_bytes = [0u8; 32];
63 x_i_bytes.copy_from_slice(&data[5..37]);
64 let fb: FieldBytes = x_i_bytes.into();
65 let x_i: NonZeroScalar = Option::from(NonZeroScalar::from_repr(fb))
66 .ok_or_else(|| scheme_error(Cmp20ErrorCode::BAD_SHARE))?;
67 let pk_bytes = &data[37..70];
68 let public_key = decode_affine(pk_bytes)?;
69 let party_idx = data[70] as u32;
70 Ok(Cmp20Share {
71 x_i,
72 public_key,
73 party_idx,
74 })
75 }
76
77 pub fn from_parts(x_i: NonZeroScalar, public_key: AffinePoint, party_idx: u32) -> Self {
78 Cmp20Share {
79 x_i,
80 public_key,
81 party_idx,
82 }
83 }
84
85 pub fn scalar(&self) -> Scalar {
86 *self.x_i
87 }
88}
89
90pub(crate) fn decode_affine(bytes: &[u8]) -> Result<AffinePoint> {
92 use elliptic_curve::point::AffineCoordinates;
93 use elliptic_curve::sec1::FromSec1Point;
94 if bytes.len() != 33 {
95 return Err(scheme_error(Cmp20ErrorCode::BAD_SHARE));
96 }
97 let enc = elliptic_curve::sec1::Sec1Point::<p256::NistP256>::from_bytes(bytes)
98 .map_err(|_| scheme_error(Cmp20ErrorCode::BAD_SHARE))?;
99 let pt: AffinePoint = Option::from(AffinePoint::from_sec1_point(&enc))
100 .ok_or_else(|| scheme_error(Cmp20ErrorCode::BAD_SHARE))?;
101 let _ = pt.x();
102 Ok(pt)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 use elliptic_curve::Generate;
110
111 fn random_share(idx: u32) -> Cmp20Share {
112 let x_i = NonZeroScalar::generate();
113 let g = p256::ProjectivePoint::GENERATOR;
114 let pk = (g * *x_i).to_affine();
115 Cmp20Share::from_parts(x_i, pk, idx)
116 }
117
118 #[test]
119 fn share_round_trip() {
120 let s = random_share(1);
121 let bytes = s.to_bytes();
122 assert_eq!(bytes.len(), SHARE_BYTES);
123 let s2 = Cmp20Share::from_bytes(&bytes).expect("decode");
124 assert_eq!(s2.party_idx, 1);
125 assert_eq!(s2.x_i.to_bytes(), s.x_i.to_bytes());
126 }
127
128 #[test]
129 fn share_rejects_bad_magic() {
130 let mut bytes = random_share(0).to_bytes();
131 bytes[0] = b'X';
132 assert!(Cmp20Share::from_bytes(&bytes).is_err());
133 }
134
135 #[test]
136 fn share_rejects_truncated() {
137 let bytes = random_share(0).to_bytes();
138 assert!(Cmp20Share::from_bytes(&bytes[..10]).is_err());
139 }
140
141 #[test]
142 fn share_rejects_zero_scalar() {
143 let mut bytes = random_share(0).to_bytes();
144 for b in &mut bytes[5..37] {
145 *b = 0;
146 }
147 assert!(Cmp20Share::from_bytes(&bytes).is_err());
148 }
149
150 #[test]
151 fn debug_redacts_secret() {
152 let s = random_share(0);
153 let dbg = format!("{:?}", s);
154 assert!(dbg.contains("<redacted>"));
155 }
156}