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 pub fn escrow(
83 &self,
84 plaintext_key: &[u8],
85 recipient: &QuorumPublicKey,
86 escrowed_by: &str,
87 key_id: &str,
88 key_type: &str,
89 encapsulator: &dyn Encapsulator,
90 aead: &dyn Aead,
91 ) -> Result<EscrowBlob, EscrowError> {
92 let metadata = EscrowMetadata::new(
93 escrowed_by,
94 key_id,
95 key_type,
96 recipient.custodian_count,
97 recipient.threshold,
98 );
99 let aad = metadata_string(&metadata);
100
101 let (encapsulated_key, shared_secret) = encapsulator.encapsulate(recipient)?;
102 let (ciphertext, nonce) = aead.encrypt(&shared_secret, plaintext_key, aad.as_bytes())?;
103
104 Ok(EscrowBlob {
105 recipient_quorum_id: recipient.quorum_id.clone(),
106 encapsulated_key,
107 ciphertext,
108 nonce,
109 aad: aad.into_bytes(),
110 metadata,
111 })
112 }
113
114 pub fn recover(
121 &self,
122 blob: &EscrowBlob,
123 shared_secret: &[u8],
124 aead: &dyn Aead,
125 ) -> Result<Vec<u8>, EscrowError> {
126 aead.decrypt(
127 shared_secret,
128 &blob.ciphertext,
129 &blob.nonce,
130 &blob.aad,
131 )
132 }
133}
134
135impl Default for EscrowService {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141fn metadata_string(m: &EscrowMetadata) -> String {
142 serde_json::to_string(m).unwrap_or_default()
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 struct MockEncapsulator;
153 impl Encapsulator for MockEncapsulator {
154 fn encapsulate(&self, recipient: &QuorumPublicKey) -> Result<(Vec<u8>, Vec<u8>), EscrowError> {
155 Ok((recipient.bytes.clone(), vec![0u8; 32]))
156 }
157 }
158
159 struct MockAead;
161 impl Aead for MockAead {
162 fn encrypt(
163 &self,
164 shared_secret: &[u8],
165 plaintext: &[u8],
166 _aad: &[u8],
167 ) -> Result<(Vec<u8>, Vec<u8>), EscrowError> {
168 let mut ct = vec![0u8; plaintext.len()];
169 for (i, b) in plaintext.iter().enumerate() {
170 ct[i] = b ^ shared_secret[i % shared_secret.len()];
171 }
172 Ok((ct, vec![0u8; 12]))
173 }
174 fn decrypt(
175 &self,
176 shared_secret: &[u8],
177 ciphertext: &[u8],
178 _nonce: &[u8],
179 _aad: &[u8],
180 ) -> Result<Vec<u8>, EscrowError> {
181 let mut pt = vec![0u8; ciphertext.len()];
182 for (i, b) in ciphertext.iter().enumerate() {
183 pt[i] = b ^ shared_secret[i % shared_secret.len()];
184 }
185 Ok(pt)
186 }
187 }
188
189 fn sample_quorum() -> QuorumPublicKey {
190 QuorumPublicKey {
191 quorum_id: "test-quorum".into(),
192 algorithm: "mock-threshold-kem".into(),
193 bytes: vec![1u8; 32],
194 custodian_count: 3,
195 threshold: 2,
196 }
197 }
198
199 #[test]
200 fn escrow_then_recover_round_trips() {
201 let service = EscrowService::new();
202 let quorum = sample_quorum();
203 let plaintext = b"this is a very secret key";
204
205 let blob = service
206 .escrow(
207 plaintext,
208 &quorum,
209 "alice",
210 "key-1",
211 "Ed25519",
212 &MockEncapsulator,
213 &MockAead,
214 )
215 .unwrap();
216
217 let shared_secret = vec![0u8; 32];
219 let recovered = service.recover(&blob, &shared_secret, &MockAead).unwrap();
220
221 assert_eq!(recovered.as_slice(), plaintext);
222 assert_eq!(blob.metadata.threshold, 2);
223 assert_eq!(blob.metadata.custodian_count, 3);
224 }
225
226 #[test]
227 fn blob_has_fingerprint() {
228 let service = EscrowService::new();
229 let blob = service
230 .escrow(
231 b"test",
232 &sample_quorum(),
233 "alice",
234 "key-1",
235 "Ed25519",
236 &MockEncapsulator,
237 &MockAead,
238 )
239 .unwrap();
240 let fp = blob.fingerprint();
241 assert_eq!(fp.len(), 32);
242 }
243}