Skip to main content

confium_privacy/
threshold_decryption.rs

1//! Threshold decryption coordinator.
2//!
3//! Coordinates ElGamal-style threshold decryption: collect decryption
4//! shares from T parties and combine them into the full plaintext.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A decryption share from one party.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct DecryptionShare {
12    pub party_idx: u32,
13    /// The partial decryption value.
14    pub share: Vec<u8>,
15}
16
17/// A threshold decryption session.
18#[derive(Debug)]
19pub struct DecryptionSession {
20    pub session_id: String,
21    pub threshold: u32,
22    pub party_count: u32,
23    pub ciphertext: Vec<u8>,
24    pub shares: HashMap<u32, DecryptionShare>,
25}
26
27impl DecryptionSession {
28    pub fn new(session_id: &str, threshold: u32, party_count: u32, ciphertext: &[u8]) -> Self {
29        Self {
30            session_id: session_id.into(),
31            threshold,
32            party_count,
33            ciphertext: ciphertext.to_vec(),
34            shares: HashMap::new(),
35        }
36    }
37
38    /// Submit a decryption share.
39    pub fn submit_share(&mut self, share: DecryptionShare) -> Result<(), String> {
40        if share.party_idx == 0 || share.party_idx > self.party_count {
41            return Err(format!("invalid party_idx: {}", share.party_idx));
42        }
43        if self.shares.contains_key(&share.party_idx) {
44            return Err(format!("party {} already submitted", share.party_idx));
45        }
46        self.shares.insert(share.party_idx, share);
47        Ok(())
48    }
49
50    /// Check if enough shares have been collected.
51    pub fn is_ready(&self) -> bool {
52        self.shares.len() >= self.threshold as usize
53    }
54
55    /// Number of shares collected.
56    pub fn share_count(&self) -> usize {
57        self.shares.len()
58    }
59
60    /// Collect the shares in party-index order for combination.
61    pub fn ordered_shares(&self) -> Vec<&DecryptionShare> {
62        let mut shares: Vec<&DecryptionShare> = self.shares.values().collect();
63        shares.sort_by_key(|s| s.party_idx);
64        shares
65    }
66
67    /// Missing party indices (those that haven't submitted).
68    pub fn missing_parties(&self) -> Vec<u32> {
69        (1..=self.party_count)
70            .filter(|i| !self.shares.contains_key(i))
71            .collect()
72    }
73}
74
75/// Errors during threshold decryption.
76#[derive(Debug, thiserror::Error)]
77pub enum DecryptionError {
78    #[error("insufficient shares: {have}/{need}")]
79    InsufficientShares { have: usize, need: u32 },
80    #[error("combination failed: {0}")]
81    CombinationFailed(String),
82}
83
84/// Combine decryption shares using Lagrange interpolation.
85/// In a real implementation, this would use the group operation
86/// (e.g., EC point addition weighted by Lagrange coefficients).
87/// Here, we XOR-combine for the mock case.
88pub fn combine_shares(session: &DecryptionSession) -> Result<Vec<u8>, DecryptionError> {
89    if !session.is_ready() {
90        return Err(DecryptionError::InsufficientShares {
91            have: session.share_count(),
92            need: session.threshold,
93        });
94    }
95
96    let shares = session.ordered_shares();
97    let max_len = shares.iter().map(|s| s.share.len()).max().unwrap_or(0);
98    let mut result = vec![0u8; max_len];
99    for share in &shares {
100        for (i, &b) in share.share.iter().enumerate() {
101            result[i] ^= b;
102        }
103    }
104    Ok(result)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn make_share(party_idx: u32, byte: u8) -> DecryptionShare {
112        DecryptionShare {
113            party_idx,
114            share: vec![byte; 32],
115        }
116    }
117
118    #[test]
119    fn new_session_empty() {
120        let session = DecryptionSession::new("s1", 2, 3, &[0; 32]);
121        assert_eq!(session.share_count(), 0);
122        assert!(!session.is_ready());
123    }
124
125    #[test]
126    fn submit_share_increments_count() {
127        let mut session = DecryptionSession::new("s1", 2, 3, &[0; 32]);
128        session.submit_share(make_share(1, 0xAA)).unwrap();
129        assert_eq!(session.share_count(), 1);
130    }
131
132    #[test]
133    fn ready_at_threshold() {
134        let mut session = DecryptionSession::new("s1", 2, 3, &[0; 32]);
135        session.submit_share(make_share(1, 0xAA)).unwrap();
136        assert!(!session.is_ready());
137        session.submit_share(make_share(2, 0xBB)).unwrap();
138        assert!(session.is_ready());
139    }
140
141    #[test]
142    fn duplicate_submission_rejected() {
143        let mut session = DecryptionSession::new("s1", 2, 3, &[0; 32]);
144        session.submit_share(make_share(1, 0xAA)).unwrap();
145        assert!(session.submit_share(make_share(1, 0xBB)).is_err());
146    }
147
148    #[test]
149    fn invalid_party_idx_rejected() {
150        let mut session = DecryptionSession::new("s1", 2, 3, &[0; 32]);
151        assert!(session.submit_share(make_share(0, 0xAA)).is_err());
152        assert!(session.submit_share(make_share(4, 0xAA)).is_err());
153    }
154
155    #[test]
156    fn combine_requires_threshold() {
157        let session = DecryptionSession::new("s1", 3, 5, &[0; 32]);
158        assert!(combine_shares(&session).is_err());
159    }
160
161    #[test]
162    fn combine_xors_shares() {
163        let mut session = DecryptionSession::new("s1", 2, 3, &[0; 32]);
164        session.submit_share(make_share(1, 0xFF)).unwrap();
165        session.submit_share(make_share(2, 0x0F)).unwrap();
166        let result = combine_shares(&session).unwrap();
167        // XOR of 0xFF and 0x0F = 0xF0
168        assert_eq!(result, vec![0xF0; 32]);
169    }
170
171    #[test]
172    fn ordered_shares_sorted() {
173        let mut session = DecryptionSession::new("s1", 3, 5, &[0; 32]);
174        session.submit_share(make_share(3, 0x33)).unwrap();
175        session.submit_share(make_share(1, 0x11)).unwrap();
176        session.submit_share(make_share(2, 0x22)).unwrap();
177        let ordered = session.ordered_shares();
178        assert_eq!(ordered[0].party_idx, 1);
179        assert_eq!(ordered[1].party_idx, 2);
180        assert_eq!(ordered[2].party_idx, 3);
181    }
182
183    #[test]
184    fn missing_parties_lists_gaps() {
185        let mut session = DecryptionSession::new("s1", 3, 5, &[0; 32]);
186        session.submit_share(make_share(1, 0xAA)).unwrap();
187        session.submit_share(make_share(3, 0xCC)).unwrap();
188        let missing = session.missing_parties();
189        assert_eq!(missing, vec![2, 4, 5]);
190    }
191}