zlayer_secrets/
cluster_dek.rs1use std::collections::HashMap;
13
14use chacha20poly1305::aead::{Aead, KeyInit};
15use chacha20poly1305::{XChaCha20Poly1305, XNonce};
16use rand::rngs::OsRng;
17use rand::TryRngCore;
18use zeroize::Zeroizing;
19
20use zlayer_types::storage::WrappedDek;
21
22use crate::sealed::{self, RecipientPrivateKey, RecipientPublicKey};
23use crate::SecretsError;
24
25pub const DEK_SIZE: usize = 32;
27
28pub const NONCE_SIZE: usize = 24;
30
31pub struct ClusterDek {
37 bytes: Zeroizing<[u8; DEK_SIZE]>,
38}
39
40impl ClusterDek {
41 #[must_use]
46 pub fn generate() -> Self {
47 let mut bytes = Zeroizing::new([0u8; DEK_SIZE]);
48 OsRng.try_fill_bytes(bytes.as_mut()).expect("OS RNG failed");
49 Self { bytes }
50 }
51
52 #[must_use]
55 pub fn from_bytes(bytes: [u8; DEK_SIZE]) -> Self {
56 let mut buf = Zeroizing::new([0u8; DEK_SIZE]);
57 buf.copy_from_slice(&bytes);
58 Self { bytes: buf }
59 }
60
61 pub fn wrap(&self, recipient: &RecipientPublicKey) -> Result<Vec<u8>, SecretsError> {
71 sealed::seal_raw(self.bytes.as_ref(), recipient)
72 .map_err(|e| SecretsError::Encryption(format!("DEK wrap failed: {e}")))
73 }
74
75 pub fn rewrap_for_set(
81 &self,
82 recipients: &HashMap<String, RecipientPublicKey>,
83 new_generation: u64,
84 ) -> Result<WrappedDek, SecretsError> {
85 let mut wraps: HashMap<String, Vec<u8>> = HashMap::with_capacity(recipients.len());
86 for (node_id, pubkey) in recipients {
87 let wrapped = self.wrap(pubkey).map_err(|e| {
88 SecretsError::Encryption(format!("DEK wrap failed for node {node_id}: {e}"))
89 })?;
90 wraps.insert(node_id.clone(), wrapped);
91 }
92 Ok(WrappedDek {
93 dek_generation: new_generation,
94 wraps,
95 })
96 }
97
98 pub fn unwrap(node_priv: &RecipientPrivateKey, wrapped: &[u8]) -> Result<Self, SecretsError> {
105 let plaintext = sealed::open_raw(wrapped, node_priv)
106 .map_err(|e| SecretsError::Decryption(format!("DEK unwrap failed: {e}")))?;
107 if plaintext.len() != DEK_SIZE {
108 return Err(SecretsError::Decryption(format!(
109 "Unwrapped DEK has wrong length: expected {DEK_SIZE} bytes, got {}",
110 plaintext.len()
111 )));
112 }
113 let mut buf = Zeroizing::new([0u8; DEK_SIZE]);
116 buf.copy_from_slice(&plaintext);
117 let mut plaintext = plaintext;
119 zeroize::Zeroize::zeroize(&mut plaintext);
120 Ok(Self { bytes: buf })
121 }
122
123 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, SecretsError> {
133 let cipher = XChaCha20Poly1305::new_from_slice(self.bytes.as_ref())
134 .map_err(|e| SecretsError::Encryption(format!("Failed to create cipher: {e}")))?;
135
136 let mut nonce_bytes = [0u8; NONCE_SIZE];
137 OsRng
138 .try_fill_bytes(&mut nonce_bytes)
139 .expect("OS RNG failed");
140 let nonce = XNonce::from_slice(&nonce_bytes);
141
142 let ciphertext = cipher
143 .encrypt(nonce, plaintext)
144 .map_err(|e| SecretsError::Encryption(format!("Encryption failed: {e}")))?;
145
146 let mut out = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
147 out.extend_from_slice(&nonce_bytes);
148 out.extend_from_slice(&ciphertext);
149 Ok(out)
150 }
151
152 pub fn decrypt(&self, blob: &[u8]) -> Result<Zeroizing<Vec<u8>>, SecretsError> {
163 if blob.len() < NONCE_SIZE {
164 return Err(SecretsError::Decryption(format!(
165 "Data too short: expected at least {NONCE_SIZE} bytes for nonce, got {}",
166 blob.len()
167 )));
168 }
169 let cipher = XChaCha20Poly1305::new_from_slice(self.bytes.as_ref())
170 .map_err(|e| SecretsError::Decryption(format!("Failed to create cipher: {e}")))?;
171
172 let (nonce_bytes, ciphertext) = blob.split_at(NONCE_SIZE);
173 let nonce = XNonce::from_slice(nonce_bytes);
174
175 let plaintext = cipher
176 .decrypt(nonce, ciphertext)
177 .map_err(|e| SecretsError::Decryption(format!("Decryption failed: {e}")))?;
178 Ok(Zeroizing::new(plaintext))
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn dek_round_trip_wrap_unwrap() {
188 let (sk, pk) = RecipientPrivateKey::generate();
189 let dek = ClusterDek::generate();
190
191 let original: [u8; DEK_SIZE] = *dek.bytes;
193
194 let wrapped = dek.wrap(&pk).expect("wrap");
195 let unwrapped = ClusterDek::unwrap(&sk, &wrapped).expect("unwrap");
196
197 assert_eq!(*unwrapped.bytes, original);
198 }
199
200 #[test]
201 fn dek_round_trip_encrypt_decrypt() {
202 let dek = ClusterDek::generate();
203 let payload = b"the rain in spain falls mainly on the plain";
204
205 let blob = dek.encrypt(payload).expect("encrypt");
206 assert!(blob.len() > NONCE_SIZE + payload.len());
208
209 let plaintext = dek.decrypt(&blob).expect("decrypt");
210 assert_eq!(plaintext.as_slice(), payload);
211 }
212
213 #[test]
214 fn dek_decrypt_tamper_detection() {
215 let dek = ClusterDek::generate();
216 let payload = b"important replicated secret";
217
218 let mut blob = dek.encrypt(payload).expect("encrypt");
219
220 let target = NONCE_SIZE + 3;
224 assert!(target < blob.len(), "blob too short to tamper");
225 blob[target] ^= 0xA5;
226
227 let result = dek.decrypt(&blob);
228 assert!(matches!(result, Err(SecretsError::Decryption(_))));
229 }
230
231 #[test]
232 fn rewrap_for_set_emits_per_node_wraps() {
233 let (sk_a, pk_a) = RecipientPrivateKey::generate();
234 let (sk_b, pk_b) = RecipientPrivateKey::generate();
235
236 let mut recipients = HashMap::new();
237 recipients.insert("node-a".to_string(), pk_a);
238 recipients.insert("node-b".to_string(), pk_b);
239
240 let dek = ClusterDek::generate();
241 let original: [u8; DEK_SIZE] = *dek.bytes;
242
243 let envelope = dek.rewrap_for_set(&recipients, 7).expect("rewrap");
244
245 assert_eq!(envelope.dek_generation, 7);
246 assert_eq!(envelope.wraps.len(), 2);
247 assert!(envelope.wraps.contains_key("node-a"));
248 assert!(envelope.wraps.contains_key("node-b"));
249
250 let unwrapped_a = ClusterDek::unwrap(&sk_a, &envelope.wraps["node-a"]).expect("unwrap a");
252 let unwrapped_b = ClusterDek::unwrap(&sk_b, &envelope.wraps["node-b"]).expect("unwrap b");
253
254 assert_eq!(*unwrapped_a.bytes, original);
255 assert_eq!(*unwrapped_b.bytes, original);
256 }
257
258 #[test]
259 fn unwrap_with_wrong_key_fails() {
260 let (_sk_a, pk_a) = RecipientPrivateKey::generate();
261 let (sk_b, _pk_b) = RecipientPrivateKey::generate();
262
263 let dek = ClusterDek::generate();
264 let wrapped = dek.wrap(&pk_a).expect("wrap to A");
265
266 let result = ClusterDek::unwrap(&sk_b, &wrapped);
267 assert!(matches!(result, Err(SecretsError::Decryption(_))));
268 }
269}