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(RevocationError::Malformed)?;
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.submissions.get_mut(submission_id).ok_or_else(|| {
83 RevocationError::Malformed(format!("unknown submission {submission_id}"))
84 })?;
85 sub.confirm_first()
86 .map_err(RevocationError::EmailVerificationFailed)
87 }
88
89 pub fn confirm_second(&mut self, submission_id: &str) -> Result<(), RevocationError> {
91 let sub = self.submissions.get_mut(submission_id).ok_or_else(|| {
92 RevocationError::Malformed(format!("unknown submission {submission_id}"))
93 })?;
94 sub.confirm_second()
95 .map_err(RevocationError::EmailVerificationFailed)
96 }
97
98 pub fn process_pending(&mut self) -> Result<usize, RevocationError> {
101 let mut count = 0;
102 for sub in self.submissions.values_mut() {
103 if sub.state == SubmissionState::SecondConfirmed {
104 sub.mark_decrypted()
105 .map_err(RevocationError::ThresholdDecryption)?;
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.submissions.get_mut(submission_id).ok_or_else(|| {
115 RevocationError::Malformed(format!("unknown submission {submission_id}"))
116 })?;
117 sub.mark_published().map_err(RevocationError::Publish)
118 }
119
120 pub fn pending_count(&self) -> usize {
122 self.submissions
123 .values()
124 .filter(|s| {
125 !matches!(
126 s.state,
127 SubmissionState::Published | SubmissionState::Cancelled
128 )
129 })
130 .count()
131 }
132
133 pub fn submission(&self, id: &str) -> Option<&Submission> {
135 self.submissions.get(id)
136 }
137}
138
139pub trait Encapsulator {
141 fn encapsulate(&self, quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String>;
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 struct MockEncapsulator;
150 impl Encapsulator for MockEncapsulator {
151 fn encapsulate(&self, _quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String> {
152 Ok((vec![0u8; 32], vec![0u8; 32]))
153 }
154 }
155
156 #[test]
157 fn service_lifecycle_mock() {
158 let mut service = RevocationService::new("tb-revocation-quorum");
159
160 let blob = service
161 .prepare_revocation_blob(
162 "alice@example.com",
163 "ABCD1234",
164 &[1u8, 2, 3, 4],
165 &[5u8, 6, 7, 8],
166 &MockEncapsulator,
167 )
168 .unwrap();
169
170 let id = service.submit(blob, "valid-token").unwrap();
171 service.confirm_first(&id).unwrap();
172
173 {
175 let sub_mut = service.submissions.get_mut(&id).unwrap();
176 sub_mut.delay_until = Some(chrono::Utc::now() - chrono::Duration::hours(1));
177 }
178
179 service.confirm_second(&id).unwrap();
180 let processed = service.process_pending().unwrap();
181 assert_eq!(processed, 1);
182 service.publish(&id).unwrap();
183
184 assert_eq!(
185 service.submission(&id).unwrap().state,
186 SubmissionState::Published
187 );
188 }
189
190 #[test]
191 fn submit_with_empty_token_fails() {
192 let mut service = RevocationService::new("q");
193 let blob = service
194 .prepare_revocation_blob("a@b", "X", &[], &[], &MockEncapsulator)
195 .unwrap();
196 let result = service.submit(blob, "");
197 assert!(matches!(result, Err(RevocationError::InvalidToken(_))));
198 }
199}