nodedb_types/backup_envelope/
crypto.rs1use aes_gcm::Aes256Gcm;
46#[allow(deprecated)]
51use aes_gcm::aead::generic_array::GenericArray;
52use aes_gcm::aead::{Aead, KeyInit};
53use sha2::{Digest, Sha256};
54
55use super::types::{Envelope, EnvelopeError, EnvelopeMeta, Section, read2, read4, read8};
56use super::types::{HEADER_LEN, MAGIC, TRAILER_LEN, VERSION};
57use super::write::{EnvelopeWriter, write_header};
58
59const CRYPTO_BLOCK_LEN: usize = 68;
63
64const ENC_SECTION_OVERHEAD: usize = 28;
68
69fn kek_fingerprint(kek: &[u8; 32]) -> [u8; 8] {
73 let hash = Sha256::digest(kek.as_slice());
74 let mut fp = [0u8; 8];
75 fp.copy_from_slice(&hash[..8]);
76 fp
77}
78
79fn random_bytes<const N: usize>() -> Result<[u8; N], EnvelopeError> {
82 let mut buf = [0u8; N];
83 getrandom::fill(&mut buf).map_err(|e| EnvelopeError::RandomFailure(e.to_string()))?;
84 Ok(buf)
85}
86
87#[allow(deprecated)]
90fn aes_encrypt(
91 key_bytes: &[u8; 32],
92 nonce_bytes: &[u8; 12],
93 plaintext: &[u8],
94) -> Result<Vec<u8>, EnvelopeError> {
95 let key = GenericArray::from(*key_bytes);
96 let cipher = Aes256Gcm::new(&key);
97 let nonce = GenericArray::from(*nonce_bytes);
98 cipher
99 .encrypt(&nonce, plaintext)
100 .map_err(|_| EnvelopeError::EncryptionFailed)
101}
102
103#[allow(deprecated)]
104fn aes_decrypt(
105 key_bytes: &[u8; 32],
106 nonce_bytes: &[u8; 12],
107 ciphertext: &[u8],
108) -> Result<Vec<u8>, EnvelopeError> {
109 let key = GenericArray::from(*key_bytes);
110 let cipher = Aes256Gcm::new(&key);
111 let nonce = GenericArray::from(*nonce_bytes);
112 cipher
113 .decrypt(&nonce, ciphertext)
114 .map_err(|_| EnvelopeError::DecryptionFailed)
115}
116
117impl EnvelopeWriter {
120 pub fn finalize_encrypted(self, kek: &[u8; 32]) -> Result<Vec<u8>, EnvelopeError> {
127 let dek: [u8; 32] = random_bytes()?;
129
130 let dek_nonce: [u8; 12] = random_bytes()?;
132 let wrapped_dek = aes_encrypt(kek, &dek_nonce, &dek)?;
133 debug_assert_eq!(wrapped_dek.len(), 48);
135
136 let fingerprint = kek_fingerprint(kek);
137
138 let mut enc_sections: Vec<(u64, [u8; 12], Vec<u8>)> =
140 Vec::with_capacity(self.sections.len());
141 for section in &self.sections {
142 let nonce: [u8; 12] = random_bytes()?;
143 let ciphertext = aes_encrypt(&dek, &nonce, §ion.body)?;
144 enc_sections.push((section.origin_node_id, nonce, ciphertext));
145 }
146
147 let mut total_size = HEADER_LEN + CRYPTO_BLOCK_LEN + TRAILER_LEN;
149 for (_, _, ct) in &enc_sections {
150 total_size += ENC_SECTION_OVERHEAD + ct.len();
151 }
152
153 let mut out = Vec::with_capacity(total_size);
154
155 write_header(&mut out, &self.meta, self.sections.len() as u16, VERSION);
157
158 out.extend_from_slice(&fingerprint);
160 out.extend_from_slice(&dek_nonce);
161 out.extend_from_slice(&wrapped_dek);
162
163 for (origin_node_id, nonce, ciphertext) in &enc_sections {
165 out.extend_from_slice(&origin_node_id.to_le_bytes());
166 out.extend_from_slice(&(ciphertext.len() as u32).to_le_bytes());
167 out.extend_from_slice(nonce);
168 out.extend_from_slice(ciphertext);
169 let body_crc = crc32c::crc32c(ciphertext);
170 out.extend_from_slice(&body_crc.to_le_bytes());
171 }
172
173 let trailer_crc = crc32c::crc32c(&out);
175 out.extend_from_slice(&trailer_crc.to_le_bytes());
176
177 Ok(out)
178 }
179}
180
181pub fn parse_encrypted(
190 bytes: &[u8],
191 max_total: u64,
192 kek: &[u8; 32],
193) -> Result<Envelope, EnvelopeError> {
194 if bytes.len() as u64 > max_total {
195 return Err(EnvelopeError::OverSizeTotal { cap: max_total });
196 }
197
198 let min_len = HEADER_LEN + CRYPTO_BLOCK_LEN + TRAILER_LEN;
199 if bytes.len() < min_len {
200 return Err(EnvelopeError::Truncated);
201 }
202
203 let header_bytes = &bytes[..HEADER_LEN];
205 if &header_bytes[0..4] != MAGIC {
206 return Err(EnvelopeError::BadMagic);
207 }
208 let version = header_bytes[4];
209 if version != VERSION {
210 return Err(EnvelopeError::UnsupportedVersion(version));
211 }
212
213 let claimed_header_crc = u32::from_le_bytes(read4(&header_bytes[48..52]));
215 let actual_header_crc = crc32c::crc32c(&header_bytes[..48]);
216 if claimed_header_crc != actual_header_crc {
217 return Err(EnvelopeError::HeaderCrcMismatch);
218 }
219
220 let meta = EnvelopeMeta {
221 tenant_id: u64::from_le_bytes(read8(&header_bytes[8..16])),
222 source_vshard_count: u16::from_le_bytes(read2(&header_bytes[16..18])),
223 hash_seed: u64::from_le_bytes(read8(&header_bytes[24..32])),
224 snapshot_watermark: u64::from_le_bytes(read8(&header_bytes[32..40])),
225 };
226 let section_count = u16::from_le_bytes(read2(&header_bytes[40..42]));
227
228 let cb_start = HEADER_LEN;
230 let cb = &bytes[cb_start..cb_start + CRYPTO_BLOCK_LEN];
231 let stored_fingerprint: [u8; 8] = cb[0..8].try_into().expect("slice is 8 bytes");
232 let dek_nonce: [u8; 12] = cb[8..20].try_into().expect("slice is 12 bytes");
233 let wrapped_dek: &[u8] = &cb[20..68]; let presented_fingerprint = kek_fingerprint(kek);
237 if presented_fingerprint != stored_fingerprint {
238 return Err(EnvelopeError::WrongBackupKek);
239 }
240
241 let dek_vec = aes_decrypt(kek, &dek_nonce, wrapped_dek)?;
243 if dek_vec.len() != 32 {
244 return Err(EnvelopeError::DecryptionFailed);
245 }
246 let mut dek = [0u8; 32];
247 dek.copy_from_slice(&dek_vec);
248
249 let trailer_start = bytes.len() - TRAILER_LEN;
251 let claimed_trailer_crc = u32::from_le_bytes(read4(&bytes[trailer_start..]));
252 let actual_trailer_crc = crc32c::crc32c(&bytes[..trailer_start]);
253 if claimed_trailer_crc != actual_trailer_crc {
254 return Err(EnvelopeError::TrailerCrcMismatch);
255 }
256
257 let mut cursor = HEADER_LEN + CRYPTO_BLOCK_LEN;
259 let mut sections = Vec::with_capacity(section_count as usize);
260
261 for _ in 0..section_count {
262 if cursor + ENC_SECTION_OVERHEAD > trailer_start {
264 return Err(EnvelopeError::Truncated);
265 }
266 let origin_node_id = u64::from_le_bytes(read8(&bytes[cursor..cursor + 8]));
267 let ct_len = u32::from_le_bytes(read4(&bytes[cursor + 8..cursor + 12])) as usize;
268 let nonce_start = cursor + 12;
269 let nonce_end = nonce_start + 12;
270 let ct_start = nonce_end;
271 let ct_end = ct_start + ct_len;
272 let crc_end = ct_end + 4;
273
274 if crc_end > trailer_start {
275 return Err(EnvelopeError::Truncated);
276 }
277
278 let ciphertext = &bytes[ct_start..ct_end];
280 let claimed_body_crc = u32::from_le_bytes(read4(&bytes[ct_end..crc_end]));
281 if crc32c::crc32c(ciphertext) != claimed_body_crc {
282 return Err(EnvelopeError::BodyCrcMismatch);
283 }
284
285 let section_nonce: [u8; 12] = bytes[nonce_start..nonce_end]
286 .try_into()
287 .expect("slice is 12 bytes");
288
289 let plaintext = aes_decrypt(&dek, §ion_nonce, ciphertext)?;
291
292 sections.push(Section {
293 origin_node_id,
294 body: plaintext,
295 });
296 cursor = crc_end;
297 }
298
299 if cursor != trailer_start {
300 return Err(EnvelopeError::Truncated);
301 }
302
303 Ok(Envelope { meta, sections })
304}
305
306#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::backup_envelope::types::{DEFAULT_MAX_TOTAL_BYTES, EnvelopeMeta};
312 use crate::backup_envelope::write::EnvelopeWriter;
313
314 fn meta() -> EnvelopeMeta {
315 EnvelopeMeta {
316 tenant_id: 77,
317 source_vshard_count: 256,
318 hash_seed: 0xCAFE,
319 snapshot_watermark: 999,
320 }
321 }
322
323 fn test_kek() -> [u8; 32] {
324 [0xA1u8; 32]
325 }
326
327 fn test_kek2() -> [u8; 32] {
328 [0xB2u8; 32]
329 }
330
331 fn make_writer_with_sections() -> EnvelopeWriter {
332 let mut w = EnvelopeWriter::new(meta());
333 w.push_section(1, b"alpha payload".to_vec()).unwrap();
334 w.push_section(2, b"beta data chunk".to_vec()).unwrap();
335 w.push_section(3, vec![]).unwrap();
336 w
337 }
338
339 #[test]
340 fn encrypted_roundtrips_with_correct_kek() {
341 let kek = test_kek();
342 let w = make_writer_with_sections();
343 let bytes = w.finalize_encrypted(&kek).unwrap();
344
345 let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap();
346 assert_eq!(env.meta, meta());
347 assert_eq!(env.sections.len(), 3);
348 assert_eq!(env.sections[0].origin_node_id, 1);
349 assert_eq!(env.sections[0].body, b"alpha payload");
350 assert_eq!(env.sections[1].origin_node_id, 2);
351 assert_eq!(env.sections[1].body, b"beta data chunk");
352 assert_eq!(env.sections[2].body, b"");
353 }
354
355 #[test]
356 fn wrong_kek_returns_wrong_backup_kek_error() {
357 let kek = test_kek();
358 let wrong_kek = test_kek2();
359
360 let w = make_writer_with_sections();
361 let bytes = w.finalize_encrypted(&kek).unwrap();
362
363 assert_eq!(
364 parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &wrong_kek).unwrap_err(),
365 EnvelopeError::WrongBackupKek,
366 );
367 }
368
369 #[test]
370 fn tampering_with_section_body_fails_auth_tag() {
371 let kek = test_kek();
372 let mut w = EnvelopeWriter::new(meta());
373 w.push_section(1, b"secret data".to_vec()).unwrap();
374 let mut bytes = w.finalize_encrypted(&kek).unwrap();
375
376 let ct_start = HEADER_LEN + CRYPTO_BLOCK_LEN + 8 + 4 + 12;
379 bytes[ct_start] ^= 0xFF; let ct_len_start = HEADER_LEN + CRYPTO_BLOCK_LEN + 8;
383 let ct_len =
384 u32::from_le_bytes(bytes[ct_len_start..ct_len_start + 4].try_into().unwrap()) as usize;
385 let crc_start = ct_start + ct_len;
386 let new_crc = crc32c::crc32c(&bytes[ct_start..crc_start]);
387 bytes[crc_start..crc_start + 4].copy_from_slice(&new_crc.to_le_bytes());
388
389 let trailer_start = bytes.len() - TRAILER_LEN;
391 let new_trailer = crc32c::crc32c(&bytes[..trailer_start]);
392 bytes[trailer_start..].copy_from_slice(&new_trailer.to_le_bytes());
393
394 let err = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap_err();
395 assert_eq!(err, EnvelopeError::DecryptionFailed);
396 }
397
398 #[test]
399 fn tampering_with_wrapped_dek_fails_decryption() {
400 let kek = test_kek();
401 let mut w = EnvelopeWriter::new(meta());
402 w.push_section(1, b"data".to_vec()).unwrap();
403 let mut bytes = w.finalize_encrypted(&kek).unwrap();
404
405 let wd_start = HEADER_LEN + 8 + 12;
407 bytes[wd_start] ^= 0xFF;
408
409 let trailer_start = bytes.len() - TRAILER_LEN;
411 let new_trailer = crc32c::crc32c(&bytes[..trailer_start]);
412 bytes[trailer_start..].copy_from_slice(&new_trailer.to_le_bytes());
413
414 let err = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap_err();
415 assert_eq!(err, EnvelopeError::DecryptionFailed);
416 }
417
418 #[test]
419 fn backup_kek_and_wal_kek_are_independent() {
420 let wal_kek = [0x11u8; 32];
425 let backup_kek = [0x22u8; 32];
426
427 assert_ne!(
428 kek_fingerprint(&wal_kek),
429 kek_fingerprint(&backup_kek),
430 "wal kek and backup kek must have different fingerprints"
431 );
432
433 let mut w = EnvelopeWriter::new(meta());
435 w.push_section(1, b"payload".to_vec()).unwrap();
436 let bytes = w.finalize_encrypted(&backup_kek).unwrap();
437
438 assert_eq!(
439 parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &wal_kek).unwrap_err(),
440 EnvelopeError::WrongBackupKek,
441 );
442
443 let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &backup_kek).unwrap();
445 assert_eq!(env.sections[0].body, b"payload");
446 }
447
448 #[test]
449 fn empty_envelope_encrypted_roundtrips() {
450 let kek = test_kek();
451 let bytes = EnvelopeWriter::new(meta())
452 .finalize_encrypted(&kek)
453 .unwrap();
454 let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap();
455 assert_eq!(env.meta, meta());
456 assert!(env.sections.is_empty());
457 }
458}