Skip to main content

confium_patterns/escrow/
service.rs

1//! Escrow service — encrypts and recovers keys.
2
3use crate::escrow::blob::EscrowBlob;
4use crate::escrow::metadata::EscrowMetadata;
5
6/// Errors during escrow operations.
7#[derive(Debug, thiserror::Error)]
8pub enum EscrowError {
9    /// Encapsulation failure.
10    #[error("encapsulation failure: {0}")]
11    Encapsulate(String),
12    /// AEAD encryption failure.
13    #[error("AEAD encryption failure: {0}")]
14    AeadEncrypt(String),
15    /// Decapsulation failure (threshold decryption ceremony failed).
16    #[error("decapsulation failure: {0}")]
17    Decapsulate(String),
18    /// Threshold not met.
19    #[error("threshold not met: have {have}, need {need}")]
20    ThresholdNotMet {
21        /// Number of partial decryptions collected.
22        have: usize,
23        /// Threshold T.
24        need: u32,
25    },
26    /// Invalid blob.
27    #[error("invalid blob: {0}")]
28    InvalidBlob(String),
29}
30
31/// Quorum public key (recipient of escrowed data).
32#[derive(Debug, Clone)]
33pub struct QuorumPublicKey {
34    /// Quorum identifier.
35    pub quorum_id: String,
36    /// Algorithm.
37    pub algorithm: String,
38    /// Raw public key bytes.
39    pub bytes: Vec<u8>,
40    /// Number of custodians N.
41    pub custodian_count: u32,
42    /// Threshold T.
43    pub threshold: u32,
44}
45
46/// Encapsulator hook — caller provides concrete threshold KEM implementation.
47pub trait Encapsulator {
48    /// Encapsulate to the quorum public key. Returns (encapsulated_key, shared_secret).
49    fn encapsulate(&self, recipient: &QuorumPublicKey) -> Result<(Vec<u8>, Vec<u8>), EscrowError>;
50}
51
52/// AEAD hook — caller provides concrete AEAD implementation.
53pub trait Aead {
54    /// Encrypt plaintext with shared_secret as key. Returns (ciphertext, nonce).
55    fn encrypt(
56        &self,
57        shared_secret: &[u8],
58        plaintext: &[u8],
59        aad: &[u8],
60    ) -> Result<(Vec<u8>, Vec<u8>), EscrowError>;
61
62    /// Decrypt ciphertext with shared_secret as key.
63    fn decrypt(
64        &self,
65        shared_secret: &[u8],
66        ciphertext: &[u8],
67        nonce: &[u8],
68        aad: &[u8],
69    ) -> Result<Vec<u8>, EscrowError>;
70}
71
72/// The escrow service.
73pub struct EscrowService;
74
75impl EscrowService {
76    /// Construct a new escrow service.
77    pub fn new() -> Self {
78        Self
79    }
80
81    /// Escrow a key (or any secret) to a recipient quorum.
82    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    /// Recover a key from a blob, given the recovered shared secret.
115    ///
116    /// The caller is responsible for running the threshold decryption
117    /// ceremony to recover the shared secret (typically via
118    /// `confium-tc-kem`). This function takes the recovered secret
119    /// and performs the final AEAD decryption.
120    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    // Lightweight deterministic encoding (canonical JSON via serde_json::to_string).
143    serde_json::to_string(m).unwrap_or_default()
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    /// Mock encapsulator: returns the public key bytes as encapsulated,
151    /// 32 zero bytes as shared secret.
152    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    /// Mock AEAD: XOR plaintext with shared_secret (truncated/extended).
160    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        // In real usage, this would come from a threshold decryption ceremony.
218        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}