1use crate::primitives::big_number::{BigNumber, Endian};
7use crate::primitives::curve::Curve;
8use crate::primitives::error::PrimitivesError;
9use crate::primitives::hash::sha512_hmac;
10use crate::primitives::polynomial::{PointInFiniteField, Polynomial};
11use crate::primitives::private_key::PrivateKey;
12use crate::primitives::random::random_bytes;
13use crate::primitives::utils::to_hex;
14
15pub struct KeyShares {
24 pub points: Vec<PointInFiniteField>,
25 pub threshold: usize,
26 pub integrity: String,
27}
28
29impl KeyShares {
30 pub fn new(points: Vec<PointInFiniteField>, threshold: usize, integrity: String) -> Self {
32 KeyShares {
33 points,
34 threshold,
35 integrity,
36 }
37 }
38
39 pub fn split(
49 key: &PrivateKey,
50 threshold: usize,
51 total: usize,
52 ) -> Result<Self, PrimitivesError> {
53 if threshold < 2 {
54 return Err(PrimitivesError::ThresholdError(
55 "threshold must be at least 2".to_string(),
56 ));
57 }
58 if total < 2 {
59 return Err(PrimitivesError::ThresholdError(
60 "totalShares must be at least 2".to_string(),
61 ));
62 }
63 if threshold > total {
64 return Err(PrimitivesError::ThresholdError(
65 "threshold should be less than or equal to totalShares".to_string(),
66 ));
67 }
68
69 let curve = Curve::secp256k1();
70 let key_bytes = key.to_bytes();
71 let poly = Polynomial::from_private_key(&key_bytes, threshold);
72
73 let mut points = Vec::with_capacity(total);
74 let mut used_x_coords: Vec<BigNumber> = Vec::new();
75
76 let seed = random_bytes(64);
79
80 for i in 0..total {
81 let mut x: BigNumber;
82 let mut attempts = 0u32;
83
84 loop {
85 let mut counter = Vec::new();
86 counter.push(i as u8);
87 counter.push(attempts as u8);
88 counter.extend_from_slice(&random_bytes(32));
89
90 let h = sha512_hmac(&seed, &counter);
91 x = BigNumber::from_bytes(&h, Endian::Big);
92 x = x
93 .umod(&curve.p)
94 .map_err(|e| PrimitivesError::ArithmeticError(format!("mod p: {e}")))?;
95
96 attempts += 1;
97 if attempts > 5 {
98 return Err(PrimitivesError::ThresholdError(
99 "Failed to generate unique x coordinate after 5 attempts".to_string(),
100 ));
101 }
102
103 if x.is_zero() {
105 continue;
106 }
107 let mut duplicate = false;
108 for existing in &used_x_coords {
109 if existing.cmp(&x) == 0 {
110 duplicate = true;
111 break;
112 }
113 }
114 if !duplicate {
115 break;
116 }
117 }
118
119 used_x_coords.push(x.clone());
120 let y = poly.value_at(&x);
121 points.push(PointInFiniteField::new(x, y));
122 }
123
124 let pubkey = key.to_public_key();
127 let pubkey_hash = pubkey.to_hash();
128 let integrity = to_hex(&pubkey_hash);
129 let integrity = integrity[..8].to_string();
130
131 Ok(KeyShares {
132 points,
133 threshold,
134 integrity,
135 })
136 }
137
138 pub fn to_backup_format(&self) -> Vec<String> {
142 self.points
143 .iter()
144 .map(|share| {
145 format!(
146 "{}.{}.{}",
147 share.to_string_repr(),
148 self.threshold,
149 self.integrity
150 )
151 })
152 .collect()
153 }
154
155 pub fn from_backup_format(shares: &[String]) -> Result<Self, PrimitivesError> {
159 if shares.is_empty() {
160 return Err(PrimitivesError::InvalidFormat(
161 "No shares provided".to_string(),
162 ));
163 }
164
165 let mut threshold = 0usize;
166 let mut integrity = String::new();
167 let mut points = Vec::with_capacity(shares.len());
168
169 for (idx, share) in shares.iter().enumerate() {
170 let parts: Vec<&str> = share.split('.').collect();
171 if parts.len() != 4 {
172 return Err(PrimitivesError::InvalidFormat(format!(
173 "Invalid share format in share {idx}. Expected format: \"x.y.t.i\" - received {share}"
174 )));
175 }
176
177 let t_str = parts[2];
178 let i_str = parts[3];
179
180 let t: usize = t_str.parse().map_err(|_| {
181 PrimitivesError::InvalidFormat(format!(
182 "Threshold not a valid number in share {idx}"
183 ))
184 })?;
185
186 if idx != 0 && threshold != t {
187 return Err(PrimitivesError::InvalidFormat(format!(
188 "Threshold mismatch in share {idx}"
189 )));
190 }
191 if idx != 0 && integrity != i_str {
192 return Err(PrimitivesError::InvalidFormat(format!(
193 "Integrity mismatch in share {idx}"
194 )));
195 }
196
197 threshold = t;
198 integrity = i_str.to_string();
199
200 let point_str = format!("{}.{}", parts[0], parts[1]);
201 let point = PointInFiniteField::from_string_repr(&point_str)?;
202 points.push(point);
203 }
204
205 Ok(KeyShares::new(points, threshold, integrity))
206 }
207
208 pub fn reconstruct(shares: &KeyShares) -> Result<PrivateKey, PrimitivesError> {
219 let threshold = shares.threshold;
220
221 if threshold < 2 {
222 return Err(PrimitivesError::ThresholdError(
223 "threshold must be at least 2".to_string(),
224 ));
225 }
226
227 if shares.points.len() < threshold {
228 return Err(PrimitivesError::ThresholdError(format!(
229 "At least {threshold} shares are required to reconstruct the private key"
230 )));
231 }
232
233 for i in 0..threshold {
235 for j in (i + 1)..threshold {
236 if shares.points[i].x.cmp(&shares.points[j].x) == 0 {
237 return Err(PrimitivesError::ThresholdError(
238 "Duplicate share detected, each must be unique.".to_string(),
239 ));
240 }
241 }
242 }
243
244 let poly = Polynomial::new(shares.points.clone(), Some(threshold));
246 let secret = poly.value_at(&BigNumber::zero());
247
248 let secret_bytes = secret.to_array(Endian::Big, Some(32));
250 let key = PrivateKey::from_bytes(&secret_bytes)?;
251
252 let pubkey = key.to_public_key();
254 let pubkey_hash = pubkey.to_hash();
255 let integrity_hash = to_hex(&pubkey_hash);
256 let integrity_check = &integrity_hash[..8];
257
258 if integrity_check != shares.integrity {
259 return Err(PrimitivesError::ThresholdError(
260 "Integrity hash mismatch".to_string(),
261 ));
262 }
263
264 Ok(key)
265 }
266}
267
268#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_key_shares_split_produces_correct_count() {
278 let key = PrivateKey::from_random().unwrap();
279 let shares = KeyShares::split(&key, 2, 5).unwrap();
280 assert_eq!(shares.points.len(), 5);
281 assert_eq!(shares.threshold, 2);
282 assert!(!shares.integrity.is_empty());
283 }
284
285 #[test]
286 fn test_key_shares_split_and_reconstruct_threshold_2_of_3() {
287 let key = PrivateKey::from_random().unwrap();
288 let shares = KeyShares::split(&key, 2, 3).unwrap();
289
290 let subset = KeyShares::new(
292 shares.points[..2].to_vec(),
293 shares.threshold,
294 shares.integrity.clone(),
295 );
296 let recovered = KeyShares::reconstruct(&subset).unwrap();
297 assert_eq!(
298 recovered.to_hex(),
299 key.to_hex(),
300 "Should recover original key from 2 of 3 shares"
301 );
302 }
303
304 #[test]
305 fn test_key_shares_split_and_reconstruct_threshold_3_of_5() {
306 let key = PrivateKey::from_random().unwrap();
307 let shares = KeyShares::split(&key, 3, 5).unwrap();
308
309 let subset = KeyShares::new(
311 vec![
312 shares.points[0].clone(),
313 shares.points[2].clone(),
314 shares.points[4].clone(),
315 ],
316 shares.threshold,
317 shares.integrity.clone(),
318 );
319 let recovered = KeyShares::reconstruct(&subset).unwrap();
320 assert_eq!(
321 recovered.to_hex(),
322 key.to_hex(),
323 "Should recover original key from 3 of 5 shares"
324 );
325 }
326
327 #[test]
328 fn test_key_shares_insufficient_shares_fails() {
329 let key = PrivateKey::from_random().unwrap();
330 let shares = KeyShares::split(&key, 3, 5).unwrap();
331
332 let subset = KeyShares::new(
334 shares.points[..2].to_vec(),
335 shares.threshold,
336 shares.integrity.clone(),
337 );
338 let result = KeyShares::reconstruct(&subset);
339 assert!(
340 result.is_err(),
341 "Should fail with fewer than threshold shares"
342 );
343 }
344
345 #[test]
346 fn test_key_shares_threshold_validation() {
347 let key = PrivateKey::from_random().unwrap();
348
349 assert!(KeyShares::split(&key, 1, 3).is_err());
351
352 assert!(KeyShares::split(&key, 2, 1).is_err());
354
355 assert!(KeyShares::split(&key, 4, 3).is_err());
357 }
358
359 #[test]
360 fn test_key_shares_backup_format_roundtrip() {
361 let key = PrivateKey::from_random().unwrap();
362 let shares = KeyShares::split(&key, 2, 3).unwrap();
363
364 let backup = shares.to_backup_format();
366 assert_eq!(backup.len(), 3);
367
368 for b in &backup {
370 let parts: Vec<&str> = b.split('.').collect();
371 assert_eq!(parts.len(), 4, "Backup format should be x.y.t.i");
372 }
373
374 let parsed = KeyShares::from_backup_format(&backup[..2]).unwrap();
376 let recovered = KeyShares::reconstruct(&parsed).unwrap();
377 assert_eq!(
378 recovered.to_hex(),
379 key.to_hex(),
380 "Should recover from backup format"
381 );
382 }
383
384 #[test]
385 fn test_key_shares_integrity_hash() {
386 let key = PrivateKey::from_random().unwrap();
387 let shares = KeyShares::split(&key, 2, 3).unwrap();
388
389 assert_eq!(
391 shares.integrity.len(),
392 8,
393 "Integrity hash should be 8 hex chars"
394 );
395
396 let backup = shares.to_backup_format();
398 for b in &backup {
399 assert!(b.ends_with(&shares.integrity));
400 }
401 }
402
403 #[test]
404 fn test_key_shares_integrity_mismatch_detected() {
405 let key = PrivateKey::from_random().unwrap();
406 let shares = KeyShares::split(&key, 2, 3).unwrap();
407
408 let corrupt_shares = KeyShares::new(
410 shares.points[..2].to_vec(),
411 shares.threshold,
412 "deadbeef".to_string(), );
414 let result = KeyShares::reconstruct(&corrupt_shares);
415 assert!(result.is_err(), "Should fail on integrity mismatch");
416 }
417
418 #[test]
419 fn test_key_shares_known_key() {
420 let key = PrivateKey::from_hex(
422 "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35",
423 )
424 .unwrap();
425
426 let shares = KeyShares::split(&key, 2, 3).unwrap();
427 let subset = KeyShares::new(
428 shares.points[1..3].to_vec(),
429 shares.threshold,
430 shares.integrity.clone(),
431 );
432 let recovered = KeyShares::reconstruct(&subset).unwrap();
433 assert_eq!(recovered.to_hex(), key.to_hex(), "Should recover known key");
434 }
435
436 #[test]
437 fn test_key_shares_invalid_backup_format() {
438 let bad = vec!["not.valid".to_string()];
439 assert!(KeyShares::from_backup_format(&bad).is_err());
440 }
441
442 #[test]
443 fn test_key_shares_any_subset_reconstructs() {
444 let key = PrivateKey::from_random().unwrap();
446 let shares = KeyShares::split(&key, 2, 4).unwrap();
447
448 for i in 0..4 {
450 for j in (i + 1)..4 {
451 let subset = KeyShares::new(
452 vec![shares.points[i].clone(), shares.points[j].clone()],
453 shares.threshold,
454 shares.integrity.clone(),
455 );
456 let recovered = KeyShares::reconstruct(&subset).unwrap();
457 assert_eq!(
458 recovered.to_hex(),
459 key.to_hex(),
460 "Shares ({i}, {j}) should reconstruct"
461 );
462 }
463 }
464 }
465}