1use crate::escrow::blob::EscrowBlob;
4use crate::escrow::metadata::EscrowMetadata;
5
6#[derive(Debug, thiserror::Error)]
8pub enum EscrowError {
9 #[error("encapsulation failure: {0}")]
11 Encapsulate(String),
12 #[error("AEAD encryption failure: {0}")]
14 AeadEncrypt(String),
15 #[error("decapsulation failure: {0}")]
17 Decapsulate(String),
18 #[error("threshold not met: have {have}, need {need}")]
20 ThresholdNotMet {
21 have: usize,
23 need: u32,
25 },
26 #[error("invalid blob: {0}")]
28 InvalidBlob(String),
29}
30
31#[derive(Debug, Clone)]
33pub struct QuorumPublicKey {
34 pub quorum_id: String,
36 pub algorithm: String,
38 pub bytes: Vec<u8>,
40 pub custodian_count: u32,
42 pub threshold: u32,
44}
45
46pub trait Encapsulator {
48 fn encapsulate(&self, recipient: &QuorumPublicKey) -> Result<(Vec<u8>, Vec<u8>), EscrowError>;
50}
51
52pub trait Aead {
54 fn encrypt(
56 &self,
57 shared_secret: &[u8],
58 plaintext: &[u8],
59 aad: &[u8],
60 ) -> Result<(Vec<u8>, Vec<u8>), EscrowError>;
61
62 fn decrypt(
64 &self,
65 shared_secret: &[u8],
66 ciphertext: &[u8],
67 nonce: &[u8],
68 aad: &[u8],
69 ) -> Result<Vec<u8>, EscrowError>;
70}
71
72pub struct EscrowService;
74
75impl EscrowService {
76 pub fn new() -> Self {
78 Self
79 }
80
81 #[allow(clippy::too_many_arguments)]
83 pub fn escrow(
84 &self,
85 plaintext_key: &[u8],
86 recipient: &QuorumPublicKey,
87 escrowed_by: &str,
88 key_id: &str,
89 key_type: &str,
90 encapsulator: &dyn Encapsulator,
91 aead: &dyn Aead,
92 ) -> Result<EscrowBlob, EscrowError> {
93 let metadata = EscrowMetadata::new(
94 escrowed_by,
95 key_id,
96 key_type,
97 recipient.custodian_count,
98 recipient.threshold,
99 );
100 let aad = metadata_string(&metadata);
101
102 let (encapsulated_key, shared_secret) = encapsulator.encapsulate(recipient)?;
103 let (ciphertext, nonce) = aead.encrypt(&shared_secret, plaintext_key, aad.as_bytes())?;
104
105 Ok(EscrowBlob {
106 recipient_quorum_id: recipient.quorum_id.clone(),
107 encapsulated_key,
108 ciphertext,
109 nonce,
110 aad: aad.into_bytes(),
111 metadata,
112 })
113 }
114
115 pub fn recover(
122 &self,
123 blob: &EscrowBlob,
124 shared_secret: &[u8],
125 aead: &dyn Aead,
126 ) -> Result<Vec<u8>, EscrowError> {
127 aead.decrypt(shared_secret, &blob.ciphertext, &blob.nonce, &blob.aad)
128 }
129}
130
131impl Default for EscrowService {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137fn metadata_string(m: &EscrowMetadata) -> String {
138 serde_json::to_string(m).unwrap_or_default()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 struct MockEncapsulator;
149 impl Encapsulator for MockEncapsulator {
150 fn encapsulate(
151 &self,
152 recipient: &QuorumPublicKey,
153 ) -> Result<(Vec<u8>, Vec<u8>), EscrowError> {
154 Ok((recipient.bytes.clone(), vec![0u8; 32]))
155 }
156 }
157
158 struct MockAead;
160 impl Aead for MockAead {
161 fn encrypt(
162 &self,
163 shared_secret: &[u8],
164 plaintext: &[u8],
165 _aad: &[u8],
166 ) -> Result<(Vec<u8>, Vec<u8>), EscrowError> {
167 let mut ct = vec![0u8; plaintext.len()];
168 for (i, b) in plaintext.iter().enumerate() {
169 ct[i] = b ^ shared_secret[i % shared_secret.len()];
170 }
171 Ok((ct, vec![0u8; 12]))
172 }
173 fn decrypt(
174 &self,
175 shared_secret: &[u8],
176 ciphertext: &[u8],
177 _nonce: &[u8],
178 _aad: &[u8],
179 ) -> Result<Vec<u8>, EscrowError> {
180 let mut pt = vec![0u8; ciphertext.len()];
181 for (i, b) in ciphertext.iter().enumerate() {
182 pt[i] = b ^ shared_secret[i % shared_secret.len()];
183 }
184 Ok(pt)
185 }
186 }
187
188 fn sample_quorum() -> QuorumPublicKey {
189 QuorumPublicKey {
190 quorum_id: "test-quorum".into(),
191 algorithm: "mock-threshold-kem".into(),
192 bytes: vec![1u8; 32],
193 custodian_count: 3,
194 threshold: 2,
195 }
196 }
197
198 #[test]
199 fn escrow_then_recover_round_trips() {
200 let service = EscrowService::new();
201 let quorum = sample_quorum();
202 let plaintext = b"this is a very secret key";
203
204 let blob = service
205 .escrow(
206 plaintext,
207 &quorum,
208 "alice",
209 "key-1",
210 "Ed25519",
211 &MockEncapsulator,
212 &MockAead,
213 )
214 .unwrap();
215
216 let shared_secret = vec![0u8; 32];
218 let recovered = service.recover(&blob, &shared_secret, &MockAead).unwrap();
219
220 assert_eq!(recovered.as_slice(), plaintext);
221 assert_eq!(blob.metadata.threshold, 2);
222 assert_eq!(blob.metadata.custodian_count, 3);
223 }
224
225 #[test]
226 fn blob_has_fingerprint() {
227 let service = EscrowService::new();
228 let blob = service
229 .escrow(
230 b"test",
231 &sample_quorum(),
232 "alice",
233 "key-1",
234 "Ed25519",
235 &MockEncapsulator,
236 &MockAead,
237 )
238 .unwrap();
239 let fp = blob.fingerprint();
240 assert_eq!(fp.len(), 32);
241 }
242}