Skip to main content

bsv_rs/primitives/bsv/
shamir.rs

1//! Shamir Secret Sharing for private key backup and recovery.
2//!
3//! This module implements Shamir's Secret Sharing scheme for splitting a private key
4//! into multiple shares, where any threshold number of shares can reconstruct the
5//! original key, but fewer shares reveal nothing about it.
6//!
7//! # Example
8//!
9//! ```rust
10//! use bsv_rs::primitives::bsv::shamir::{split_private_key, KeyShares};
11//! use bsv_rs::primitives::ec::PrivateKey;
12//!
13//! // Generate a random private key
14//! let key = PrivateKey::random();
15//!
16//! // Split into 5 shares with threshold of 3
17//! let shares = split_private_key(&key, 3, 5).unwrap();
18//!
19//! // Export to backup format
20//! let backup = shares.to_backup_format();
21//! assert_eq!(backup.len(), 5);
22//!
23//! // Recover from any 3 shares
24//! let subset = KeyShares::from_backup_format(&backup[0..3]).unwrap();
25//! let recovered = subset.recover_private_key().unwrap();
26//!
27//! assert_eq!(key.to_bytes(), recovered.to_bytes());
28//! ```
29//!
30//! # Backup Format
31//!
32//! Each share is serialized as: `base58(x).base58(y).threshold.integrity`
33//!
34//! - `base58(x)` and `base58(y)` are the point coordinates
35//! - `threshold` is the minimum number of shares needed for recovery
36//! - `integrity` is the first 4 characters of base58(sha256(secret)) for verification
37
38use crate::error::{Error, Result};
39use crate::primitives::bsv::polynomial::{PointInFiniteField, Polynomial};
40use crate::primitives::encoding::{from_base58, to_base58};
41use crate::primitives::hash::sha256;
42use crate::primitives::BigNumber;
43use crate::primitives::PrivateKey;
44
45/// A collection of key shares that can be used to recover a private key.
46///
47/// Contains the share points, the threshold needed for recovery, and an
48/// integrity checksum to verify successful recovery.
49#[derive(Clone, Debug)]
50pub struct KeyShares {
51    /// The share points (x, y coordinates in the finite field).
52    pub points: Vec<PointInFiniteField>,
53    /// The minimum number of shares needed for recovery.
54    pub threshold: usize,
55    /// Integrity check: first 4 characters of base58(sha256(secret)).
56    pub integrity: String,
57}
58
59impl KeyShares {
60    /// Creates a new KeyShares instance.
61    ///
62    /// # Arguments
63    ///
64    /// * `points` - The share points
65    /// * `threshold` - The minimum number of shares needed for recovery
66    /// * `integrity` - The integrity checksum string
67    pub fn new(points: Vec<PointInFiniteField>, threshold: usize, integrity: String) -> Self {
68        Self {
69            points,
70            threshold,
71            integrity,
72        }
73    }
74
75    /// Parses key shares from backup format strings.
76    ///
77    /// Each share string must be in the format: `base58(x).base58(y).threshold.integrity`
78    ///
79    /// # Arguments
80    ///
81    /// * `shares` - The backup format strings
82    ///
83    /// # Returns
84    ///
85    /// The parsed KeyShares, or an error if parsing fails or shares are inconsistent
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if:
90    /// - Any share string has an invalid format
91    /// - Shares have different thresholds
92    /// - Shares have different integrity values
93    ///
94    /// # Example
95    ///
96    /// ```rust
97    /// use bsv_rs::primitives::bsv::shamir::KeyShares;
98    ///
99    /// let backup = vec![
100    ///     "2.someY.3.abcd".to_string(),
101    ///     "3.otherY.3.abcd".to_string(),
102    ///     "4.anotherY.3.abcd".to_string(),
103    /// ];
104    /// // Note: This example would fail because "someY" etc. aren't valid base58
105    /// // In practice, use actual share strings from split_private_key()
106    /// ```
107    pub fn from_backup_format(shares: &[String]) -> Result<Self> {
108        if shares.is_empty() {
109            return Err(Error::CryptoError(
110                "No shares provided for recovery".to_string(),
111            ));
112        }
113
114        let mut points = Vec::with_capacity(shares.len());
115        let mut threshold: Option<usize> = None;
116        let mut integrity: Option<String> = None;
117
118        for (idx, share) in shares.iter().enumerate() {
119            let (point, t, i) = decode_share(share)?;
120
121            // Validate consistency
122            if let Some(existing_threshold) = threshold {
123                if existing_threshold != t {
124                    return Err(Error::CryptoError(format!(
125                        "Threshold mismatch: share 0 has threshold {}, share {} has threshold {}",
126                        existing_threshold, idx, t
127                    )));
128                }
129            } else {
130                threshold = Some(t);
131            }
132
133            if let Some(ref existing_integrity) = integrity {
134                if existing_integrity != &i {
135                    return Err(Error::CryptoError(format!(
136                        "Integrity mismatch: share 0 has integrity '{}', share {} has integrity '{}'",
137                        existing_integrity, idx, i
138                    )));
139                }
140            } else {
141                integrity = Some(i);
142            }
143
144            points.push(point);
145        }
146
147        Ok(Self {
148            points,
149            threshold: threshold.unwrap(),
150            integrity: integrity.unwrap(),
151        })
152    }
153
154    /// Converts the key shares to backup format strings.
155    ///
156    /// Each share is serialized as: `base58(x).base58(y).threshold.integrity`
157    ///
158    /// # Returns
159    ///
160    /// A vector of backup format strings, one per share
161    ///
162    /// # Example
163    ///
164    /// ```rust
165    /// use bsv_rs::primitives::bsv::shamir::split_private_key;
166    /// use bsv_rs::primitives::ec::PrivateKey;
167    ///
168    /// let key = PrivateKey::random();
169    /// let shares = split_private_key(&key, 2, 3).unwrap();
170    /// let backup = shares.to_backup_format();
171    ///
172    /// // Each string can be stored separately
173    /// for (i, share_str) in backup.iter().enumerate() {
174    ///     println!("Share {}: {}", i + 1, share_str);
175    /// }
176    /// ```
177    pub fn to_backup_format(&self) -> Vec<String> {
178        self.points
179            .iter()
180            .map(|point| {
181                format!(
182                    "{}.{}.{}",
183                    point.to_point_string(),
184                    self.threshold,
185                    self.integrity
186                )
187            })
188            .collect()
189    }
190
191    /// Recovers the private key from the shares using Lagrange interpolation.
192    ///
193    /// The secret is the y-intercept (value at x=0) of the polynomial that passes
194    /// through all the share points.
195    ///
196    /// # Returns
197    ///
198    /// The recovered private key, or an error if:
199    /// - There are fewer shares than the threshold
200    /// - The integrity check fails
201    /// - The recovered value is not a valid private key
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// use bsv_rs::primitives::bsv::shamir::split_private_key;
207    /// use bsv_rs::primitives::ec::PrivateKey;
208    ///
209    /// let key = PrivateKey::random();
210    /// let shares = split_private_key(&key, 3, 5).unwrap();
211    ///
212    /// // Recover from exactly 3 shares
213    /// let recovered = shares.recover_private_key().unwrap();
214    /// assert_eq!(key.to_bytes(), recovered.to_bytes());
215    /// ```
216    pub fn recover_private_key(&self) -> Result<PrivateKey> {
217        if self.points.len() < self.threshold {
218            return Err(Error::CryptoError(format!(
219                "Insufficient shares: have {}, need {}",
220                self.points.len(),
221                self.threshold
222            )));
223        }
224
225        // Create polynomial from points and evaluate at x=0
226        let poly = Polynomial::new(self.points.clone(), self.threshold);
227        let secret = poly.value_at(&BigNumber::zero());
228
229        // Convert to 32-byte representation
230        // The secret should fit in 32 bytes (it's a private key value)
231        let secret_bytes = secret.to_bytes_be(32);
232
233        // Create the private key
234        let key = PrivateKey::from_bytes(&secret_bytes)?;
235
236        // Verify integrity
237        let computed_integrity = compute_integrity(&key);
238        if computed_integrity != self.integrity {
239            return Err(Error::CryptoError(format!(
240                "Integrity check failed: computed '{}', expected '{}'",
241                computed_integrity, self.integrity
242            )));
243        }
244
245        Ok(key)
246    }
247}
248
249/// Splits a private key into multiple shares using Shamir's Secret Sharing.
250///
251/// The secret (private key) becomes the constant term of a random polynomial.
252/// Shares are generated by evaluating the polynomial at x = 1, 2, 3, ..., total.
253///
254/// # Arguments
255///
256/// * `key` - The private key to split
257/// * `threshold` - The minimum number of shares needed for recovery (must be >= 2)
258/// * `total` - The total number of shares to generate (must be >= threshold)
259///
260/// # Returns
261///
262/// The generated key shares, or an error if the parameters are invalid
263///
264/// # Example
265///
266/// ```rust
267/// use bsv_rs::primitives::bsv::shamir::split_private_key;
268/// use bsv_rs::primitives::ec::PrivateKey;
269///
270/// let key = PrivateKey::random();
271///
272/// // Create 5 shares where any 3 can recover the key
273/// let shares = split_private_key(&key, 3, 5).unwrap();
274///
275/// assert_eq!(shares.points.len(), 5);
276/// assert_eq!(shares.threshold, 3);
277/// ```
278///
279/// # Security
280///
281/// - Choose threshold based on your security requirements
282/// - Higher threshold = more shares needed = more secure against partial compromise
283/// - Lower threshold = easier to recover = less secure
284/// - Typical values: 2-of-3, 3-of-5, etc.
285pub fn split_private_key(key: &PrivateKey, threshold: usize, total: usize) -> Result<KeyShares> {
286    // Validate parameters
287    if threshold < 2 {
288        return Err(Error::CryptoError(
289            "Threshold must be at least 2".to_string(),
290        ));
291    }
292    if total < threshold {
293        return Err(Error::CryptoError(format!(
294            "Total shares ({}) must be at least threshold ({})",
295            total, threshold
296        )));
297    }
298    if threshold > 255 {
299        return Err(Error::CryptoError(
300            "Threshold cannot exceed 255".to_string(),
301        ));
302    }
303
304    let p = BigNumber::secp256k1_prime();
305
306    // The secret is the private key as a BigNumber
307    let secret = BigNumber::from_bytes_be(&key.to_bytes());
308
309    // Generate random polynomial coefficients a_1, a_2, ..., a_{t-1}
310    // The constant term a_0 is the secret
311    let mut coefficients = Vec::with_capacity(threshold);
312    coefficients.push(secret);
313
314    for _ in 1..threshold {
315        // Generate random 32-byte coefficient
316        let random_key = PrivateKey::random();
317        let coeff = BigNumber::from_bytes_be(&random_key.to_bytes()).modulo(&p);
318        coefficients.push(coeff);
319    }
320
321    // Generate shares by evaluating polynomial at x = 1, 2, ..., total
322    let mut points = Vec::with_capacity(total);
323
324    for i in 1..=total {
325        let x = BigNumber::from_u64(i as u64);
326        let y = evaluate_polynomial(&coefficients, &x, &p);
327        points.push(PointInFiniteField::new(x, y));
328    }
329
330    // Compute integrity checksum
331    let integrity = compute_integrity(key);
332
333    Ok(KeyShares {
334        points,
335        threshold,
336        integrity,
337    })
338}
339
340/// Evaluates a polynomial at a given point.
341///
342/// Given coefficients [a_0, a_1, ..., a_{n-1}], computes:
343/// f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_{n-1}*x^{n-1}
344///
345/// Uses Horner's method for efficiency.
346fn evaluate_polynomial(
347    coefficients: &[BigNumber],
348    x: &BigNumber,
349    modulus: &BigNumber,
350) -> BigNumber {
351    // Horner's method: f(x) = a_0 + x*(a_1 + x*(a_2 + ... + x*a_{n-1}))
352    let mut result = BigNumber::zero();
353
354    for coeff in coefficients.iter().rev() {
355        result = result.mul(x).add(coeff).modulo(modulus);
356    }
357
358    result
359}
360
361/// Computes the integrity checksum for a private key.
362///
363/// Returns the first 4 characters of base58(sha256(key_bytes)).
364fn compute_integrity(key: &PrivateKey) -> String {
365    let hash = sha256(&key.to_bytes());
366    let b58 = to_base58(&hash);
367
368    // Take first 4 characters
369    if b58.len() >= 4 {
370        b58[..4].to_string()
371    } else {
372        b58
373    }
374}
375
376/// Decodes a share from its backup format string.
377///
378/// Format: `base58(x).base58(y).threshold.integrity`
379///
380/// Returns the point, threshold, and integrity string.
381fn decode_share(share: &str) -> Result<(PointInFiniteField, usize, String)> {
382    let components: Vec<&str> = share.split('.').collect();
383
384    if components.len() != 4 {
385        return Err(Error::CryptoError(format!(
386            "Invalid share format: expected 'base58(x).base58(y).threshold.integrity', got '{}'",
387            share
388        )));
389    }
390
391    // Parse x and y from base58
392    let x_bytes = from_base58(components[0])?;
393    let y_bytes = from_base58(components[1])?;
394
395    let x = BigNumber::from_bytes_be(&x_bytes);
396    let y = BigNumber::from_bytes_be(&y_bytes);
397
398    let point = PointInFiniteField::new(x, y);
399
400    // Parse threshold
401    let threshold: usize = components[2].parse().map_err(|e| {
402        Error::CryptoError(format!(
403            "Invalid threshold in share: {} ({})",
404            components[2], e
405        ))
406    })?;
407
408    // Integrity is the last component
409    let integrity = components[3].to_string();
410
411    Ok((point, threshold, integrity))
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_split_recover_roundtrip() {
420        let key = PrivateKey::random();
421        let shares = split_private_key(&key, 3, 5).unwrap();
422
423        assert_eq!(shares.points.len(), 5);
424        assert_eq!(shares.threshold, 3);
425
426        // Recover from first 3 shares
427        let subset = KeyShares::new(shares.points[0..3].to_vec(), 3, shares.integrity.clone());
428        let recovered = subset.recover_private_key().unwrap();
429        assert_eq!(key.to_bytes(), recovered.to_bytes());
430    }
431
432    #[test]
433    fn test_split_recover_different_subsets() {
434        let key = PrivateKey::random();
435        let shares = split_private_key(&key, 3, 5).unwrap();
436
437        // Try different subsets of 3 shares
438        let subsets = vec![
439            vec![0, 1, 2],
440            vec![0, 1, 3],
441            vec![0, 1, 4],
442            vec![0, 2, 3],
443            vec![0, 2, 4],
444            vec![0, 3, 4],
445            vec![1, 2, 3],
446            vec![1, 2, 4],
447            vec![1, 3, 4],
448            vec![2, 3, 4],
449        ];
450
451        for indices in subsets {
452            let points: Vec<_> = indices.iter().map(|&i| shares.points[i].clone()).collect();
453            let subset = KeyShares::new(points, 3, shares.integrity.clone());
454            let recovered = subset.recover_private_key().unwrap();
455            assert_eq!(
456                key.to_bytes(),
457                recovered.to_bytes(),
458                "Failed for indices {:?}",
459                indices
460            );
461        }
462    }
463
464    #[test]
465    fn test_backup_format_roundtrip() {
466        let key = PrivateKey::random();
467        let shares = split_private_key(&key, 3, 5).unwrap();
468
469        // Export to backup format
470        let backup = shares.to_backup_format();
471        assert_eq!(backup.len(), 5);
472
473        // Each backup string should have 4 parts
474        for s in &backup {
475            assert_eq!(s.split('.').count(), 4);
476        }
477
478        // Restore from backup (using middle 3 shares)
479        let restored = KeyShares::from_backup_format(&backup[1..4]).unwrap();
480        let recovered = restored.recover_private_key().unwrap();
481        assert_eq!(key.to_bytes(), recovered.to_bytes());
482    }
483
484    #[test]
485    fn test_minimum_threshold() {
486        // Test with threshold = 2
487        let key = PrivateKey::random();
488        let shares = split_private_key(&key, 2, 3).unwrap();
489
490        let subset = KeyShares::new(shares.points[0..2].to_vec(), 2, shares.integrity.clone());
491        let recovered = subset.recover_private_key().unwrap();
492        assert_eq!(key.to_bytes(), recovered.to_bytes());
493    }
494
495    #[test]
496    fn test_exact_threshold_equals_total() {
497        // Test with threshold = total (all shares needed)
498        let key = PrivateKey::random();
499        let shares = split_private_key(&key, 5, 5).unwrap();
500
501        let recovered = shares.recover_private_key().unwrap();
502        assert_eq!(key.to_bytes(), recovered.to_bytes());
503    }
504
505    #[test]
506    fn test_insufficient_shares() {
507        let key = PrivateKey::random();
508        let shares = split_private_key(&key, 3, 5).unwrap();
509
510        // Try to recover with only 2 shares (less than threshold)
511        let subset = KeyShares::new(shares.points[0..2].to_vec(), 3, shares.integrity.clone());
512        let result = subset.recover_private_key();
513        assert!(matches!(result, Err(Error::CryptoError(_))));
514    }
515
516    #[test]
517    fn test_invalid_threshold() {
518        let key = PrivateKey::random();
519
520        // Threshold less than 2
521        assert!(matches!(
522            split_private_key(&key, 1, 5),
523            Err(Error::CryptoError(_))
524        ));
525        assert!(matches!(
526            split_private_key(&key, 0, 5),
527            Err(Error::CryptoError(_))
528        ));
529
530        // Total less than threshold
531        assert!(matches!(
532            split_private_key(&key, 5, 3),
533            Err(Error::CryptoError(_))
534        ));
535    }
536
537    #[test]
538    fn test_integrity_check_fails_on_corruption() {
539        let key = PrivateKey::random();
540        let shares = split_private_key(&key, 2, 3).unwrap();
541
542        // Create shares with wrong integrity
543        let corrupted = KeyShares::new(
544            shares.points.clone(),
545            2,
546            "XXXX".to_string(), // Wrong integrity
547        );
548
549        let result = corrupted.recover_private_key();
550        assert!(matches!(result, Err(Error::CryptoError(_))));
551        assert!(result
552            .unwrap_err()
553            .to_string()
554            .contains("Integrity check failed"));
555    }
556
557    #[test]
558    fn test_backup_format_parsing() {
559        let key = PrivateKey::random();
560        let shares = split_private_key(&key, 2, 3).unwrap();
561        let backup = shares.to_backup_format();
562
563        // Verify format
564        let parsed = KeyShares::from_backup_format(&backup).unwrap();
565        assert_eq!(parsed.threshold, shares.threshold);
566        assert_eq!(parsed.integrity, shares.integrity);
567        assert_eq!(parsed.points.len(), shares.points.len());
568    }
569
570    #[test]
571    fn test_mismatched_threshold_in_shares() {
572        // Create two shares manually with different thresholds
573        let share1 = "2.abc.3.XXXX".to_string();
574        let share2 = "3.def.4.XXXX".to_string(); // Different threshold
575
576        let result = KeyShares::from_backup_format(&[share1, share2]);
577        assert!(matches!(result, Err(Error::CryptoError(_))));
578        assert!(result
579            .unwrap_err()
580            .to_string()
581            .contains("Threshold mismatch"));
582    }
583
584    #[test]
585    fn test_mismatched_integrity_in_shares() {
586        // Create two shares manually with different integrity
587        let share1 = "2.abc.3.AAAA".to_string();
588        let share2 = "3.def.3.BBBB".to_string(); // Different integrity
589
590        let result = KeyShares::from_backup_format(&[share1, share2]);
591        assert!(matches!(result, Err(Error::CryptoError(_))));
592        assert!(result
593            .unwrap_err()
594            .to_string()
595            .contains("Integrity mismatch"));
596    }
597
598    #[test]
599    fn test_known_private_key() {
600        // Test with a known private key value
601        let key = PrivateKey::from_hex(
602            "0000000000000000000000000000000000000000000000000000000000000001",
603        )
604        .unwrap();
605
606        let shares = split_private_key(&key, 2, 3).unwrap();
607        let recovered = shares.recover_private_key().unwrap();
608
609        assert_eq!(key.to_bytes(), recovered.to_bytes());
610    }
611
612    #[test]
613    fn test_large_number_of_shares() {
614        let key = PrivateKey::random();
615        let shares = split_private_key(&key, 5, 10).unwrap();
616
617        assert_eq!(shares.points.len(), 10);
618
619        // Recover from any 5 shares
620        let subset = KeyShares::new(
621            shares.points[5..10].to_vec(), // Last 5 shares
622            5,
623            shares.integrity.clone(),
624        );
625        let recovered = subset.recover_private_key().unwrap();
626        assert_eq!(key.to_bytes(), recovered.to_bytes());
627    }
628
629    #[test]
630    fn test_compute_integrity() {
631        let key = PrivateKey::from_hex(
632            "0000000000000000000000000000000000000000000000000000000000000001",
633        )
634        .unwrap();
635
636        let integrity = compute_integrity(&key);
637        // Should be 4 characters
638        assert_eq!(integrity.len(), 4);
639
640        // Integrity should be deterministic
641        let integrity2 = compute_integrity(&key);
642        assert_eq!(integrity, integrity2);
643    }
644
645    #[test]
646    fn test_evaluate_polynomial() {
647        let p = BigNumber::secp256k1_prime();
648
649        // Test polynomial f(x) = 5 + 3x + 2x^2
650        // f(0) = 5, f(1) = 10, f(2) = 19
651        let coefficients = vec![
652            BigNumber::from_u64(5),
653            BigNumber::from_u64(3),
654            BigNumber::from_u64(2),
655        ];
656
657        assert_eq!(
658            evaluate_polynomial(&coefficients, &BigNumber::zero(), &p),
659            BigNumber::from_u64(5)
660        );
661        assert_eq!(
662            evaluate_polynomial(&coefficients, &BigNumber::from_u64(1), &p),
663            BigNumber::from_u64(10)
664        );
665        assert_eq!(
666            evaluate_polynomial(&coefficients, &BigNumber::from_u64(2), &p),
667            BigNumber::from_u64(19)
668        );
669    }
670
671    #[test]
672    fn test_decode_share() {
673        // Create a valid share format
674        let key = PrivateKey::random();
675        let shares = split_private_key(&key, 2, 3).unwrap();
676        let backup = shares.to_backup_format();
677
678        let (point, threshold, integrity) = decode_share(&backup[0]).unwrap();
679        assert_eq!(threshold, 2);
680        assert_eq!(integrity, shares.integrity);
681        assert_eq!(point.x, shares.points[0].x);
682        assert_eq!(point.y, shares.points[0].y);
683    }
684
685    #[test]
686    fn test_decode_share_invalid_format() {
687        // Too few parts
688        assert!(matches!(decode_share("a.b.c"), Err(Error::CryptoError(_))));
689
690        // Too many parts
691        assert!(matches!(
692            decode_share("a.b.c.d.e"),
693            Err(Error::CryptoError(_))
694        ));
695
696        // Invalid threshold
697        assert!(matches!(
698            decode_share("2.abc.notanumber.XXXX"),
699            Err(Error::CryptoError(_))
700        ));
701    }
702
703    #[test]
704    fn test_empty_shares() {
705        let result = KeyShares::from_backup_format(&[]);
706        assert!(matches!(result, Err(Error::CryptoError(_))));
707    }
708
709    // ========================
710    // Edge case tests (GAP-06)
711    // ========================
712
713    #[test]
714    fn test_threshold_greater_than_total_shares() {
715        // Mirrors Go: TestThresholdLargerThanTotalShares
716        let key = PrivateKey::random();
717        let result = split_private_key(&key, 50, 5);
718        assert!(matches!(result, Err(Error::CryptoError(_))));
719        assert!(
720            result.unwrap_err().to_string().contains("must be at least"),
721            "Expected error about total shares being less than threshold"
722        );
723    }
724
725    #[test]
726    fn test_total_shares_less_than_2() {
727        // Mirrors Go: TestTotalSharesLessThanTwo
728        let key = PrivateKey::random();
729
730        // total=1 with threshold=2 should fail (total < threshold)
731        let result = split_private_key(&key, 2, 1);
732        assert!(matches!(result, Err(Error::CryptoError(_))));
733
734        // total=1 with threshold=1 should also fail (threshold < 2)
735        let result = split_private_key(&key, 1, 1);
736        assert!(matches!(result, Err(Error::CryptoError(_))));
737    }
738
739    #[test]
740    fn test_duplicate_shares_in_recovery() {
741        // Mirrors Go: TestDuplicateShareDetected
742        // Providing the same share twice should result in failed recovery
743        // (the Lagrange interpolation will produce incorrect results or fail)
744        let key = PrivateKey::random();
745        let shares = split_private_key(&key, 3, 5).unwrap();
746        let backup = shares.to_backup_format();
747
748        // Use share 0, share 1, and share 1 again (duplicate)
749        let recovery = KeyShares::from_backup_format(&[
750            backup[0].clone(),
751            backup[1].clone(),
752            backup[1].clone(),
753        ])
754        .unwrap();
755
756        // Recovery should fail: either the interpolation gives wrong result
757        // (integrity check fails) or the mod_inverse fails on duplicate x coords
758        let result = recovery.recover_private_key();
759        assert!(
760            matches!(result, Err(Error::CryptoError(_))),
761            "Expected CryptoError when using duplicate shares for recovery, got {:?}",
762            result
763        );
764    }
765
766    #[test]
767    fn test_fewer_points_than_threshold() {
768        // Mirrors Go: TestFewerPointsThanThreshold
769        // Explicitly test the error message when fewer shares than threshold
770        let key = PrivateKey::random();
771        let shares = split_private_key(&key, 3, 5).unwrap();
772
773        // Manually set only 2 points but keep threshold at 3
774        let subset = KeyShares::new(shares.points[..2].to_vec(), 3, shares.integrity.clone());
775        let result = subset.recover_private_key();
776        assert!(matches!(result, Err(Error::CryptoError(_))));
777        assert!(
778            result
779                .unwrap_err()
780                .to_string()
781                .contains("Insufficient shares"),
782            "Expected 'Insufficient shares' error message"
783        );
784    }
785
786    #[test]
787    fn test_consistency_across_multiple_splits() {
788        // Mirrors Go: TestPolynomialConsistency
789        // Splitting the same secret twice gives different shares (randomness)
790        // but both sets should recover the same secret
791        let key = PrivateKey::random();
792
793        let shares1 = split_private_key(&key, 3, 5).unwrap();
794        let shares2 = split_private_key(&key, 3, 5).unwrap();
795
796        // The shares themselves should be different (different random polynomials)
797        assert_ne!(
798            shares1.points[0].y, shares2.points[0].y,
799            "Two splits of the same key should produce different shares due to randomness"
800        );
801
802        // But both should recover the same key
803        let recovered1 = KeyShares::new(shares1.points[..3].to_vec(), 3, shares1.integrity.clone())
804            .recover_private_key()
805            .unwrap();
806        let recovered2 = KeyShares::new(shares2.points[..3].to_vec(), 3, shares2.integrity.clone())
807            .recover_private_key()
808            .unwrap();
809
810        assert_eq!(key.to_bytes(), recovered1.to_bytes());
811        assert_eq!(key.to_bytes(), recovered2.to_bytes());
812
813        // Integrity should also match since it's derived from the same key
814        assert_eq!(shares1.integrity, shares2.integrity);
815    }
816
817    #[test]
818    fn test_different_recovery_subsets() {
819        // Mirrors Go: TestPolynomialReconstructionWithDifferentSubsets
820        // Split into 5 shares with threshold 3, recover using all C(5,3)=10 subsets
821        let key = PrivateKey::random();
822        let shares = split_private_key(&key, 3, 5).unwrap();
823
824        let all_subsets: Vec<Vec<usize>> = vec![
825            vec![0, 1, 2],
826            vec![0, 1, 3],
827            vec![0, 1, 4],
828            vec![0, 2, 3],
829            vec![0, 2, 4],
830            vec![0, 3, 4],
831            vec![1, 2, 3],
832            vec![1, 2, 4],
833            vec![1, 3, 4],
834            vec![2, 3, 4],
835        ];
836
837        for subset_indices in &all_subsets {
838            let subset_points: Vec<_> = subset_indices
839                .iter()
840                .map(|&i| shares.points[i].clone())
841                .collect();
842            let subset = KeyShares::new(subset_points, 3, shares.integrity.clone());
843            let recovered = subset.recover_private_key().unwrap();
844            assert_eq!(
845                key.to_bytes(),
846                recovered.to_bytes(),
847                "Recovery failed for subset {:?}",
848                subset_indices
849            );
850        }
851    }
852
853    #[test]
854    fn test_single_share_threshold() {
855        // Threshold of 1 should be rejected (1-of-N is just copying the secret)
856        let key = PrivateKey::random();
857        let result = split_private_key(&key, 1, 5);
858        assert!(matches!(result, Err(Error::CryptoError(_))));
859        assert!(
860            result.unwrap_err().to_string().contains("at least 2"),
861            "Expected error about threshold being at least 2"
862        );
863    }
864
865    #[test]
866    fn test_max_shares() {
867        // Test with the maximum allowed number of shares (255)
868        let key = PrivateKey::random();
869        let shares = split_private_key(&key, 3, 255).unwrap();
870        assert_eq!(shares.points.len(), 255);
871
872        // Recover from first 3 shares
873        let subset = KeyShares::new(shares.points[..3].to_vec(), 3, shares.integrity.clone());
874        let recovered = subset.recover_private_key().unwrap();
875        assert_eq!(key.to_bytes(), recovered.to_bytes());
876
877        // Recover from last 3 shares
878        let subset = KeyShares::new(
879            shares.points[252..255].to_vec(),
880            3,
881            shares.integrity.clone(),
882        );
883        let recovered = subset.recover_private_key().unwrap();
884        assert_eq!(key.to_bytes(), recovered.to_bytes());
885
886        // Recover from widely spaced shares (first, middle, last)
887        let subset = KeyShares::new(
888            vec![
889                shares.points[0].clone(),
890                shares.points[127].clone(),
891                shares.points[254].clone(),
892            ],
893            3,
894            shares.integrity.clone(),
895        );
896        let recovered = subset.recover_private_key().unwrap();
897        assert_eq!(key.to_bytes(), recovered.to_bytes());
898    }
899
900    #[test]
901    fn test_threshold_exceeds_255() {
902        // Threshold > 255 should be rejected
903        let key = PrivateKey::random();
904        let result = split_private_key(&key, 256, 300);
905        assert!(matches!(result, Err(Error::CryptoError(_))));
906        assert!(
907            result.unwrap_err().to_string().contains("255"),
908            "Expected error about threshold exceeding 255"
909        );
910    }
911
912    #[test]
913    fn test_different_thresholds_and_shares() {
914        // Mirrors Go: TestPolynomialDifferentThresholdsAndShares
915        // Test various threshold/total combinations
916        let test_cases = vec![(2, 3), (2, 5), (3, 5), (4, 7), (5, 10), (10, 10)];
917
918        for (threshold, total) in test_cases {
919            let key = PrivateKey::random();
920            let shares = split_private_key(&key, threshold, total).unwrap();
921            assert_eq!(shares.points.len(), total);
922
923            let subset = KeyShares::new(
924                shares.points[..threshold].to_vec(),
925                threshold,
926                shares.integrity.clone(),
927            );
928            let recovered = subset.recover_private_key().unwrap();
929            assert_eq!(
930                key.to_bytes(),
931                recovered.to_bytes(),
932                "Failed for threshold={}, total={}",
933                threshold,
934                total
935            );
936        }
937    }
938
939    #[test]
940    fn test_recovery_with_more_shares_than_threshold() {
941        // Providing more shares than the threshold should still work
942        let key = PrivateKey::random();
943        let shares = split_private_key(&key, 3, 5).unwrap();
944
945        // Use all 5 shares even though only 3 are needed
946        let recovered = shares.recover_private_key().unwrap();
947        assert_eq!(key.to_bytes(), recovered.to_bytes());
948
949        // Use 4 shares
950        let subset = KeyShares::new(shares.points[..4].to_vec(), 3, shares.integrity.clone());
951        let recovered = subset.recover_private_key().unwrap();
952        assert_eq!(key.to_bytes(), recovered.to_bytes());
953    }
954
955    #[test]
956    fn test_recovery_with_wrong_shares_fails_integrity() {
957        // Shares from different keys should fail integrity check
958        let key1 = PrivateKey::random();
959        let key2 = PrivateKey::random();
960
961        let shares1 = split_private_key(&key1, 2, 3).unwrap();
962        let shares2 = split_private_key(&key2, 2, 3).unwrap();
963
964        // Mix shares from two different keys but use integrity from key1
965        let mixed = KeyShares::new(
966            vec![shares1.points[0].clone(), shares2.points[1].clone()],
967            2,
968            shares1.integrity.clone(),
969        );
970        let result = mixed.recover_private_key();
971        // Should fail with integrity check error (or invalid private key)
972        assert!(
973            matches!(result, Err(Error::CryptoError(_))),
974            "Expected CryptoError when mixing shares from different keys, got {:?}",
975            result
976        );
977    }
978
979    #[test]
980    fn test_multiple_recovery_iterations() {
981        // Mirrors Go: TestPolynomialConsistency - run multiple iterations
982        // to ensure randomness doesn't cause flaky behavior
983        for _ in 0..10 {
984            let key = PrivateKey::random();
985            let shares = split_private_key(&key, 3, 5).unwrap();
986            let subset = KeyShares::new(shares.points[..3].to_vec(), 3, shares.integrity.clone());
987            let recovered = subset.recover_private_key().unwrap();
988            assert_eq!(key.to_bytes(), recovered.to_bytes());
989        }
990    }
991
992    #[test]
993    fn test_backup_recovery_full_roundtrip() {
994        // Mirrors Go: TestPrivateKeyToKeyShares - full backup/recovery cycle
995        for _ in 0..3 {
996            let key = PrivateKey::random();
997            let shares = split_private_key(&key, 3, 5).unwrap();
998            let backup = shares.to_backup_format();
999            assert_eq!(backup.len(), 5);
1000
1001            // Recover from first 3 backup strings
1002            let recovered_shares = KeyShares::from_backup_format(&backup[..3]).unwrap();
1003            let recovered_key = recovered_shares.recover_private_key().unwrap();
1004            assert_eq!(key.to_bytes(), recovered_key.to_bytes());
1005        }
1006    }
1007
1008    #[test]
1009    fn test_zero_threshold() {
1010        // Threshold of 0 should be rejected
1011        let key = PrivateKey::random();
1012        let result = split_private_key(&key, 0, 5);
1013        assert!(matches!(result, Err(Error::CryptoError(_))));
1014    }
1015
1016    #[test]
1017    fn test_zero_total_shares() {
1018        // Total of 0 should be rejected (threshold >= 2 > 0)
1019        let key = PrivateKey::random();
1020        let result = split_private_key(&key, 2, 0);
1021        assert!(matches!(result, Err(Error::CryptoError(_))));
1022    }
1023}