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 section_capacity = super::read::checked_section_capacity(
262 section_count,
263 trailer_start.saturating_sub(cursor),
264 ENC_SECTION_OVERHEAD + 16,
265 )?;
266 let mut sections = Vec::with_capacity(section_capacity);
267
268 for _ in 0..section_count {
269 if cursor + ENC_SECTION_OVERHEAD > trailer_start {
271 return Err(EnvelopeError::Truncated);
272 }
273 let origin_node_id = u64::from_le_bytes(read8(&bytes[cursor..cursor + 8]));
274 let ct_len = u32::from_le_bytes(read4(&bytes[cursor + 8..cursor + 12])) as usize;
275 let nonce_start = cursor + 12;
276 let nonce_end = nonce_start + 12;
277 let ct_start = nonce_end;
278 let ct_end = ct_start + ct_len;
279 let crc_end = ct_end + 4;
280
281 if crc_end > trailer_start {
282 return Err(EnvelopeError::Truncated);
283 }
284
285 let ciphertext = &bytes[ct_start..ct_end];
287 let claimed_body_crc = u32::from_le_bytes(read4(&bytes[ct_end..crc_end]));
288 if crc32c::crc32c(ciphertext) != claimed_body_crc {
289 return Err(EnvelopeError::BodyCrcMismatch);
290 }
291
292 let section_nonce: [u8; 12] = bytes[nonce_start..nonce_end]
293 .try_into()
294 .expect("slice is 12 bytes");
295
296 let plaintext = aes_decrypt(&dek, §ion_nonce, ciphertext)?;
298
299 sections.push(Section {
300 origin_node_id,
301 body: plaintext,
302 });
303 cursor = crc_end;
304 }
305
306 if cursor != trailer_start {
307 return Err(EnvelopeError::Truncated);
308 }
309
310 Ok(Envelope { meta, sections })
311}
312
313#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::backup_envelope::types::{DEFAULT_MAX_TOTAL_BYTES, EnvelopeMeta};
319 use crate::backup_envelope::write::EnvelopeWriter;
320
321 fn meta() -> EnvelopeMeta {
322 EnvelopeMeta {
323 tenant_id: 77,
324 source_vshard_count: 256,
325 hash_seed: 0xCAFE,
326 snapshot_watermark: 999,
327 }
328 }
329
330 fn test_kek() -> [u8; 32] {
331 [0xA1u8; 32]
332 }
333
334 fn test_kek2() -> [u8; 32] {
335 [0xB2u8; 32]
336 }
337
338 fn make_writer_with_sections() -> EnvelopeWriter {
339 let mut w = EnvelopeWriter::new(meta());
340 w.push_section(1, b"alpha payload".to_vec()).unwrap();
341 w.push_section(2, b"beta data chunk".to_vec()).unwrap();
342 w.push_section(3, vec![]).unwrap();
343 w
344 }
345
346 #[test]
347 fn encrypted_parse_rejects_huge_section_count_with_tiny_body_before_allocation() {
348 let kek = test_kek();
349 let mut bytes = EnvelopeWriter::new(meta())
350 .finalize_encrypted(&kek)
351 .unwrap();
352 bytes[40..42].copy_from_slice(&u16::MAX.to_le_bytes());
353 let header_crc = crc32c::crc32c(&bytes[..48]);
354 bytes[48..52].copy_from_slice(&header_crc.to_le_bytes());
355 let trailer_start = bytes.len() - TRAILER_LEN;
356 let trailer_crc = crc32c::crc32c(&bytes[..trailer_start]);
357 bytes[trailer_start..].copy_from_slice(&trailer_crc.to_le_bytes());
358 assert_eq!(
359 parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek),
360 Err(EnvelopeError::Truncated)
361 );
362 }
363
364 #[test]
365 fn encrypted_roundtrips_with_correct_kek() {
366 let kek = test_kek();
367 let w = make_writer_with_sections();
368 let bytes = w.finalize_encrypted(&kek).unwrap();
369
370 let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap();
371 assert_eq!(env.meta, meta());
372 assert_eq!(env.sections.len(), 3);
373 assert_eq!(env.sections[0].origin_node_id, 1);
374 assert_eq!(env.sections[0].body, b"alpha payload");
375 assert_eq!(env.sections[1].origin_node_id, 2);
376 assert_eq!(env.sections[1].body, b"beta data chunk");
377 assert_eq!(env.sections[2].body, b"");
378 }
379
380 #[test]
381 fn wrong_kek_returns_wrong_backup_kek_error() {
382 let kek = test_kek();
383 let wrong_kek = test_kek2();
384
385 let w = make_writer_with_sections();
386 let bytes = w.finalize_encrypted(&kek).unwrap();
387
388 assert_eq!(
389 parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &wrong_kek).unwrap_err(),
390 EnvelopeError::WrongBackupKek,
391 );
392 }
393
394 #[test]
395 fn tampering_with_section_body_fails_auth_tag() {
396 let kek = test_kek();
397 let mut w = EnvelopeWriter::new(meta());
398 w.push_section(1, b"secret data".to_vec()).unwrap();
399 let mut bytes = w.finalize_encrypted(&kek).unwrap();
400
401 let ct_start = HEADER_LEN + CRYPTO_BLOCK_LEN + 8 + 4 + 12;
404 bytes[ct_start] ^= 0xFF; let ct_len_start = HEADER_LEN + CRYPTO_BLOCK_LEN + 8;
408 let ct_len =
409 u32::from_le_bytes(bytes[ct_len_start..ct_len_start + 4].try_into().unwrap()) as usize;
410 let crc_start = ct_start + ct_len;
411 let new_crc = crc32c::crc32c(&bytes[ct_start..crc_start]);
412 bytes[crc_start..crc_start + 4].copy_from_slice(&new_crc.to_le_bytes());
413
414 let trailer_start = bytes.len() - TRAILER_LEN;
416 let new_trailer = crc32c::crc32c(&bytes[..trailer_start]);
417 bytes[trailer_start..].copy_from_slice(&new_trailer.to_le_bytes());
418
419 let err = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap_err();
420 assert_eq!(err, EnvelopeError::DecryptionFailed);
421 }
422
423 #[test]
424 fn tampering_with_wrapped_dek_fails_decryption() {
425 let kek = test_kek();
426 let mut w = EnvelopeWriter::new(meta());
427 w.push_section(1, b"data".to_vec()).unwrap();
428 let mut bytes = w.finalize_encrypted(&kek).unwrap();
429
430 let wd_start = HEADER_LEN + 8 + 12;
432 bytes[wd_start] ^= 0xFF;
433
434 let trailer_start = bytes.len() - TRAILER_LEN;
436 let new_trailer = crc32c::crc32c(&bytes[..trailer_start]);
437 bytes[trailer_start..].copy_from_slice(&new_trailer.to_le_bytes());
438
439 let err = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap_err();
440 assert_eq!(err, EnvelopeError::DecryptionFailed);
441 }
442
443 #[test]
444 fn backup_kek_and_wal_kek_are_independent() {
445 let wal_kek = [0x11u8; 32];
450 let backup_kek = [0x22u8; 32];
451
452 assert_ne!(
453 kek_fingerprint(&wal_kek),
454 kek_fingerprint(&backup_kek),
455 "wal kek and backup kek must have different fingerprints"
456 );
457
458 let mut w = EnvelopeWriter::new(meta());
460 w.push_section(1, b"payload".to_vec()).unwrap();
461 let bytes = w.finalize_encrypted(&backup_kek).unwrap();
462
463 assert_eq!(
464 parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &wal_kek).unwrap_err(),
465 EnvelopeError::WrongBackupKek,
466 );
467
468 let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &backup_kek).unwrap();
470 assert_eq!(env.sections[0].body, b"payload");
471 }
472
473 #[test]
474 fn empty_envelope_encrypted_roundtrips() {
475 let kek = test_kek();
476 let bytes = EnvelopeWriter::new(meta())
477 .finalize_encrypted(&kek)
478 .unwrap();
479 let env = parse_encrypted(&bytes, DEFAULT_MAX_TOTAL_BYTES, &kek).unwrap();
480 assert_eq!(env.meta, meta());
481 assert!(env.sections.is_empty());
482 }
483}