1use crate::kem::encapsulate::{EncapsulateError, EncapsulatedKey};
8use crate::kem::share::ThresholdShare;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct KemSessionParams {
15 pub algorithm: String,
17 pub quorum_id: String,
19 pub threshold: u32,
21 pub num_parties: u32,
23 pub this_party_idx: u32,
25 pub encapsulated_key: EncapsulatedKey,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum KemSessionState {
33 Pending,
35 Round1Complete,
37 Completed,
39 Expired,
41 Aborted,
43}
44
45pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct PartialDecryption {
58 pub party_index: u32,
60 pub bytes: Vec<u8>,
62}
63
64#[derive(Debug, thiserror::Error)]
66pub enum KemError {
67 #[error("encapsulate error: {0}")]
69 Encapsulate(#[from] EncapsulateError),
70 #[error("session in wrong state: {current:?}, expected {expected}")]
72 InvalidState {
73 current: KemSessionState,
75 expected: &'static str,
77 },
78 #[error("local share not set")]
80 MissingShare,
81 #[error("threshold not met: have {have}, need {need}")]
83 ThresholdNotMet {
84 have: usize,
86 need: u32,
88 },
89 #[error("algorithm mismatch: share={share}, session={session}")]
91 AlgorithmMismatch {
92 share: String,
94 session: String,
96 },
97}
98
99impl KemSession {
100 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 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 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 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 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 pub fn state(&self) -> KemSessionState {
165 self.state
166 }
167
168 pub fn created_at(&self) -> DateTime<Utc> {
170 self.created_at
171 }
172
173 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 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}