confium_patterns/revocation/
revocation_service.rs1use std::collections::HashMap;
4
5use crate::revocation::revocation_blob::{RevocationBlob, RevocationError};
6use crate::revocation::revocation_submission::{Submission, SubmissionState};
7
8pub struct RevocationService {
10 service_quorum_id: String,
11 submissions: HashMap<String, Submission>,
12}
13
14impl RevocationService {
15 pub fn new(quorum_id: impl Into<String>) -> Self {
17 Self {
18 service_quorum_id: quorum_id.into(),
19 submissions: HashMap::new(),
20 }
21 }
22
23 pub fn quorum_id(&self) -> &str {
25 &self.service_quorum_id
26 }
27
28 pub fn prepare_revocation_blob(
33 &self,
34 user_email: &str,
35 key_fingerprint: &str,
36 revocation_signature: &[u8],
37 public_key: &[u8],
38 encapsulator: &dyn Encapsulator,
39 ) -> Result<RevocationBlob, RevocationError> {
40 let (encapsulated_key, shared_secret) = encapsulator
41 .encapsulate(&self.service_quorum_id)
42 .map_err(|e| RevocationError::Malformed(e))?;
43
44 let mut payload = Vec::new();
46 payload.extend_from_slice(revocation_signature);
47 payload.extend_from_slice(public_key);
48 let ciphertext: Vec<u8> = payload
49 .iter()
50 .enumerate()
51 .map(|(i, b)| b ^ shared_secret[i % shared_secret.len()])
52 .collect();
53
54 Ok(RevocationBlob {
55 user_email: user_email.into(),
56 key_fingerprint: key_fingerprint.into(),
57 encapsulated_key,
58 ciphertext,
59 nonce: vec![0u8; 12],
60 })
61 }
62
63 pub fn submit(
65 &mut self,
66 blob: RevocationBlob,
67 verification_token: &str,
68 ) -> Result<String, RevocationError> {
69 if verification_token.is_empty() {
70 return Err(RevocationError::InvalidToken(
71 "token must be non-empty".into(),
72 ));
73 }
74 let id = format!("sub-{}", self.submissions.len() + 1);
75 let submission = Submission::new(id.clone(), blob);
76 self.submissions.insert(id.clone(), submission);
77 Ok(id)
78 }
79
80 pub fn confirm_first(&mut self, submission_id: &str) -> Result<(), RevocationError> {
82 let sub = self
83 .submissions
84 .get_mut(submission_id)
85 .ok_or_else(|| RevocationError::Malformed(format!("unknown submission {submission_id}")))?;
86 sub.confirm_first().map_err(|e| RevocationError::EmailVerificationFailed(e))
87 }
88
89 pub fn confirm_second(&mut self, submission_id: &str) -> Result<(), RevocationError> {
91 let sub = self
92 .submissions
93 .get_mut(submission_id)
94 .ok_or_else(|| RevocationError::Malformed(format!("unknown submission {submission_id}")))?;
95 sub.confirm_second()
96 .map_err(|e| RevocationError::EmailVerificationFailed(e))
97 }
98
99 pub fn process_pending(&mut self) -> Result<usize, RevocationError> {
102 let mut count = 0;
103 for sub in self.submissions.values_mut() {
104 if sub.state == SubmissionState::SecondConfirmed {
105 sub.mark_decrypted().map_err(|e| RevocationError::ThresholdDecryption(e))?;
106 count += 1;
107 }
108 }
109 Ok(count)
110 }
111
112 pub fn publish(&mut self, submission_id: &str) -> Result<(), RevocationError> {
114 let sub = self
115 .submissions
116 .get_mut(submission_id)
117 .ok_or_else(|| RevocationError::Malformed(format!("unknown submission {submission_id}")))?;
118 sub.mark_published()
119 .map_err(|e| RevocationError::Publish(e))
120 }
121
122 pub fn pending_count(&self) -> usize {
124 self.submissions
125 .values()
126 .filter(|s| !matches!(s.state, SubmissionState::Published | SubmissionState::Cancelled))
127 .count()
128 }
129
130 pub fn submission(&self, id: &str) -> Option<&Submission> {
132 self.submissions.get(id)
133 }
134}
135
136pub trait Encapsulator {
138 fn encapsulate(&self, quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String>;
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 struct MockEncapsulator;
147 impl Encapsulator for MockEncapsulator {
148 fn encapsulate(&self, _quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String> {
149 Ok((vec![0u8; 32], vec![0u8; 32]))
150 }
151 }
152
153 #[test]
154 fn service_lifecycle_mock() {
155 let mut service = RevocationService::new("tb-revocation-quorum");
156
157 let blob = service
158 .prepare_revocation_blob(
159 "alice@example.com",
160 "ABCD1234",
161 &[1u8, 2, 3, 4],
162 &[5u8, 6, 7, 8],
163 &MockEncapsulator,
164 )
165 .unwrap();
166
167 let id = service.submit(blob, "valid-token").unwrap();
168 service.confirm_first(&id).unwrap();
169
170 {
172 let sub_mut = service.submissions.get_mut(&id).unwrap();
173 sub_mut.delay_until = Some(chrono::Utc::now() - chrono::Duration::hours(1));
174 }
175
176 service.confirm_second(&id).unwrap();
177 let processed = service.process_pending().unwrap();
178 assert_eq!(processed, 1);
179 service.publish(&id).unwrap();
180
181 assert_eq!(service.submission(&id).unwrap().state, SubmissionState::Published);
182 }
183
184 #[test]
185 fn submit_with_empty_token_fails() {
186 let mut service = RevocationService::new("q");
187 let blob = service
188 .prepare_revocation_blob("a@b", "X", &[], &[], &MockEncapsulator)
189 .unwrap();
190 let result = service.submit(blob, "");
191 assert!(matches!(result, Err(RevocationError::InvalidToken(_))));
192 }
193}