Skip to main content

confium_tc/kem/
session.rs

1//! Threshold KEM decapsulation session.
2//!
3//! State machine parallel to `confium-tc::Session` but for decryption.
4//! Each party holds a share; T-of-N collaborate via the coordinator to
5//! decapsulate a shared secret that can then AEAD-decrypt the ciphertext.
6
7use crate::kem::encapsulate::{EncapsulateError, EncapsulatedKey};
8use crate::kem::share::ThresholdShare;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12/// Parameters for a decapsulation session.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct KemSessionParams {
15    /// Algorithm identifier (must match encapsulated key).
16    pub algorithm: String,
17    /// Quorum identifier (which quorum's shares are being used).
18    pub quorum_id: String,
19    /// Threshold T.
20    pub threshold: u32,
21    /// Total number of parties N.
22    pub num_parties: u32,
23    /// This party's index (0-based).
24    pub this_party_idx: u32,
25    /// The encapsulated key to decapsulate.
26    pub encapsulated_key: EncapsulatedKey,
27}
28
29/// State of a decapsulation session.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum KemSessionState {
33    /// Session created, awaiting round 1 messages.
34    Pending,
35    /// Round 1 complete, awaiting round 2 messages.
36    Round1Complete,
37    /// Decapsulation complete; shared secret available.
38    Completed,
39    /// Session expired before completion.
40    Expired,
41    /// Session aborted due to error.
42    Aborted,
43}
44
45/// A decapsulation session.
46pub struct KemSession {
47    params: KemSessionParams,
48    state: KemSessionState,
49    local_share: Option<ThresholdShare>,
50    partial_decryptions: Vec<PartialDecryption>,
51    result: Option<Vec<u8>>,
52    created_at: DateTime<Utc>,
53}
54
55/// A single party's partial decryption contribution.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct PartialDecryption {
58    /// Contributing party index.
59    pub party_index: u32,
60    /// Partial decryption bytes (algorithm-specific).
61    pub bytes: Vec<u8>,
62}
63
64/// Errors during decapsulation.
65#[derive(Debug, thiserror::Error)]
66pub enum KemError {
67    /// Wrap encapsulate-side errors.
68    #[error("encapsulate error: {0}")]
69    Encapsulate(#[from] EncapsulateError),
70    /// Session in wrong state for operation.
71    #[error("session in wrong state: {current:?}, expected {expected}")]
72    InvalidState {
73        /// Current state.
74        current: KemSessionState,
75        /// Expected state(s).
76        expected: &'static str,
77    },
78    /// Local share missing.
79    #[error("local share not set")]
80    MissingShare,
81    /// Threshold not met.
82    #[error("threshold not met: have {have}, need {need}")]
83    ThresholdNotMet {
84        /// Number of partial decryptions collected.
85        have: usize,
86        /// Threshold T.
87        need: u32,
88    },
89    /// Algorithm mismatch.
90    #[error("algorithm mismatch: share={share}, session={session}")]
91    AlgorithmMismatch {
92        /// Share algorithm.
93        share: String,
94        /// Session algorithm.
95        session: String,
96    },
97}
98
99impl KemSession {
100    /// Create a new decapsulation session.
101    pub fn new(params: KemSessionParams) -> Self {
102        Self {
103            params,
104            state: KemSessionState::Pending,
105            local_share: None,
106            partial_decryptions: Vec::new(),
107            result: None,
108            created_at: Utc::now(),
109        }
110    }
111
112    /// Provide the local share. Must be called before round 1.
113    pub fn set_local_share(&mut self, share: ThresholdShare) -> Result<(), KemError> {
114        if share.algorithm != self.params.algorithm {
115            return Err(KemError::AlgorithmMismatch {
116                share: share.algorithm,
117                session: self.params.algorithm.clone(),
118            });
119        }
120        self.local_share = Some(share);
121        Ok(())
122    }
123
124    /// Submit a partial decryption from another party.
125    pub fn submit_partial(&mut self, partial: PartialDecryption) -> Result<(), KemError> {
126        if self.state != KemSessionState::Pending && self.state != KemSessionState::Round1Complete {
127            return Err(KemError::InvalidState {
128                current: self.state,
129                expected: "pending or round1_complete",
130            });
131        }
132        self.partial_decryptions.push(partial);
133        Ok(())
134    }
135
136    /// Try to complete the decapsulation.
137    pub fn try_complete(&mut self) -> Result<Vec<u8>, KemError> {
138        let needed = self.params.threshold as usize;
139        if self.partial_decryptions.len() < needed {
140            return Err(KemError::ThresholdNotMet {
141                have: self.partial_decryptions.len(),
142                need: self.params.threshold,
143            });
144        }
145        if self.local_share.is_none() {
146            return Err(KemError::MissingShare);
147        }
148
149        // Mock implementation: XOR all partial decryptions together.
150        // Real algorithm crates provide the actual decapsulation logic.
151        let mut combined = vec![0u8; 32];
152        for partial in &self.partial_decryptions {
153            for (i, b) in partial.bytes.iter().take(32).enumerate() {
154                combined[i] ^= b;
155            }
156        }
157
158        self.result = Some(combined.clone());
159        self.state = KemSessionState::Completed;
160        Ok(combined)
161    }
162
163    /// Current session state.
164    pub fn state(&self) -> KemSessionState {
165        self.state
166    }
167
168    /// When the session was created.
169    pub fn created_at(&self) -> DateTime<Utc> {
170        self.created_at
171    }
172
173    /// Session parameters.
174    pub fn params(&self) -> &KemSessionParams {
175        &self.params
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::kem::encapsulate::EncapsulatedKey;
183
184    fn sample_params() -> KemSessionParams {
185        KemSessionParams {
186            algorithm: "mock-threshold-kem".into(),
187            quorum_id: "test-quorum".into(),
188            threshold: 2,
189            num_parties: 3,
190            this_party_idx: 0,
191            encapsulated_key: EncapsulatedKey {
192                algorithm: "mock-threshold-kem".into(),
193                bytes: vec![0u8; 32],
194            },
195        }
196    }
197
198    #[test]
199    fn session_lifecycle_mock() {
200        let mut session = KemSession::new(sample_params());
201        let share = ThresholdShare::new("mock-threshold-kem", 0, vec![1u8; 32]);
202        session.set_local_share(share).unwrap();
203
204        session
205            .submit_partial(PartialDecryption {
206                party_index: 1,
207                bytes: vec![0xAA; 32],
208            })
209            .unwrap();
210        session
211            .submit_partial(PartialDecryption {
212                party_index: 2,
213                bytes: vec![0x55; 32],
214            })
215            .unwrap();
216
217        let result = session.try_complete().unwrap();
218        assert_eq!(result.len(), 32);
219        // XOR of 0xAA and 0x55 = 0xFF
220        assert_eq!(result[0], 0xFF);
221        assert_eq!(session.state(), KemSessionState::Completed);
222    }
223
224    #[test]
225    fn threshold_not_met_fails() {
226        let mut session = KemSession::new(sample_params());
227        let share = ThresholdShare::new("mock-threshold-kem", 0, vec![1u8; 32]);
228        session.set_local_share(share).unwrap();
229        session
230            .submit_partial(PartialDecryption {
231                party_index: 1,
232                bytes: vec![0xAA; 32],
233            })
234            .unwrap();
235        let result = session.try_complete();
236        assert!(matches!(result, Err(KemError::ThresholdNotMet { .. })));
237    }
238
239    #[test]
240    fn algorithm_mismatch_rejected() {
241        let mut session = KemSession::new(sample_params());
242        let bad_share = ThresholdShare::new("different-alg", 0, vec![]);
243        let result = session.set_local_share(bad_share);
244        assert!(matches!(result, Err(KemError::AlgorithmMismatch { .. })));
245    }
246}