1use crate::{
2 errors::{FileError, HeaderError},
3 file::FileMetadata,
4 memory::SecureKey,
5 v3::{crypt, key::KeyDerivationParams, metadata},
6};
7
8#[derive(Debug, Clone)]
39pub struct FileHeader {
40 salt: [u8; 16],
41 kdf_params: KeyDerivationParams,
42 nonce_prefix: [u8; 16],
43 chunk_size: u32,
44 metadata_nonce: [u8; 24],
45 metadata_ciphertext: Vec<u8>,
46}
47
48pub const MAGIC: [u8; 6] = *b"SHADOW";
49pub const VERSION: u8 = 3;
50
51pub const MAX_ACCEPTED_CHUNK_SIZE: u32 = 64 * 1024 * 1024; impl FileHeader {
56 pub fn new(
61 salt: [u8; 16],
62 kdf_params: KeyDerivationParams,
63 nonce_prefix: [u8; 16],
64 chunk_size: u32,
65 metadata_nonce: [u8; 24],
66 metadata_ciphertext: Vec<u8>,
67 ) -> Result<Self, HeaderError> {
68 if u16::try_from(metadata_ciphertext.len()).is_err() {
69 return Err(HeaderError::MetadataTooLong);
70 }
71 if chunk_size == 0 || chunk_size > MAX_ACCEPTED_CHUNK_SIZE {
72 return Err(HeaderError::InvalidData);
73 }
74
75 Ok(FileHeader {
76 salt,
77 kdf_params,
78 nonce_prefix,
79 chunk_size,
80 metadata_nonce,
81 metadata_ciphertext,
82 })
83 }
84
85 pub(crate) const fn min_length() -> usize {
88 6 + 1 + 4 + 16 + 4 + 4 + 4 + 1 + 16 + 4 + 24 + 2 }
101
102 pub fn header_length(&self) -> usize {
104 Self::min_length() + self.metadata_ciphertext.len()
105 }
106
107 pub fn serialize(&self) -> Vec<u8> {
108 let mut bytes = Vec::with_capacity(self.header_length());
109
110 bytes.extend_from_slice(&MAGIC);
111 bytes.push(VERSION);
112 bytes.extend_from_slice(&(self.header_length() as u32).to_le_bytes());
113 bytes.extend_from_slice(&self.salt);
114 bytes.extend_from_slice(&self.kdf_params.memory_cost.to_le_bytes());
115 bytes.extend_from_slice(&self.kdf_params.time_cost.to_le_bytes());
116 bytes.extend_from_slice(&self.kdf_params.parallelism.to_le_bytes());
117 bytes.push(self.kdf_params.key_size);
118 bytes.extend_from_slice(&self.nonce_prefix);
119 bytes.extend_from_slice(&self.chunk_size.to_le_bytes());
120 bytes.extend_from_slice(&self.metadata_nonce);
121 bytes.extend_from_slice(&(self.metadata_ciphertext.len() as u16).to_le_bytes());
122 bytes.extend_from_slice(&self.metadata_ciphertext);
123
124 bytes
125 }
126
127 pub fn try_deserialize(bytes: &[u8]) -> Result<FileHeader, HeaderError> {
128 if bytes.len() < FileHeader::min_length() {
129 return Err(HeaderError::InsufficientBytes);
130 }
131
132 let length = read_header_length(bytes)?;
133
134 if bytes.len() < length as usize {
135 return Err(HeaderError::InsufficientBytes);
136 }
137
138 match Self::deserialize(bytes) {
139 Some(header) => Ok(header),
140 None => Err(HeaderError::InvalidData),
141 }
142 }
143
144 fn deserialize(bytes: &[u8]) -> Option<FileHeader> {
145 if bytes.len() < FileHeader::min_length() {
146 return None;
147 }
148 let magic: [u8; 6] = bytes[0..6].try_into().ok()?;
149 let version = bytes[6];
150
151 if magic != MAGIC || version != VERSION {
152 return None;
153 }
154
155 let header_length = u32::from_le_bytes(bytes[7..11].try_into().ok()?);
156 let salt = bytes[11..27].try_into().ok()?;
157 let kdf_memory = u32::from_le_bytes(bytes[27..31].try_into().ok()?);
158 let kdf_iterations = u32::from_le_bytes(bytes[31..35].try_into().ok()?);
159 let kdf_parallelism = u32::from_le_bytes(bytes[35..39].try_into().ok()?);
160 let kdf_key_length = bytes[39];
161 let nonce_prefix = bytes[40..56].try_into().ok()?;
162 let chunk_size = u32::from_le_bytes(bytes[56..60].try_into().ok()?);
163 let metadata_nonce = bytes[60..84].try_into().ok()?;
164 let metadata_ciphertext_length = u16::from_le_bytes(bytes[84..86].try_into().ok()?);
165
166 if chunk_size == 0 || chunk_size > MAX_ACCEPTED_CHUNK_SIZE {
167 return None;
168 }
169
170 let expected_length: usize = FileHeader::min_length() + metadata_ciphertext_length as usize;
171
172 if header_length != expected_length as u32 {
173 return None;
174 }
175
176 if bytes.len() < expected_length {
177 return None;
178 }
179
180 let metadata_ciphertext = bytes[FileHeader::min_length()..expected_length].to_vec();
181
182 Some(FileHeader {
183 salt,
184 kdf_params: KeyDerivationParams::new(
185 kdf_memory,
186 kdf_iterations,
187 kdf_parallelism,
188 kdf_key_length,
189 ),
190 nonce_prefix,
191 chunk_size,
192 metadata_nonce,
193 metadata_ciphertext,
194 })
195 }
196
197 pub fn salt(&self) -> &[u8; 16] {
198 &self.salt
199 }
200
201 pub fn kdf_params(&self) -> &KeyDerivationParams {
203 &self.kdf_params
204 }
205
206 pub fn nonce_prefix(&self) -> &[u8; 16] {
207 &self.nonce_prefix
208 }
209
210 pub fn chunk_size(&self) -> u32 {
211 self.chunk_size
212 }
213
214 pub fn metadata_nonce(&self) -> &[u8; 24] {
215 &self.metadata_nonce
216 }
217
218 pub fn metadata_ciphertext(&self) -> &[u8] {
219 &self.metadata_ciphertext
220 }
221
222 pub fn decrypt_metadata(&self, key: &SecureKey) -> Result<FileMetadata, FileError> {
225 let (envelope, _) = crypt::decrypt_bytes(
226 &self.metadata_ciphertext,
227 key.as_bytes(),
228 &self.metadata_nonce,
229 &self.binding().aad(AadPurpose::Metadata),
230 )?;
231 metadata::parse(envelope.as_slice())
232 }
233
234 pub fn binding(&self) -> HeaderBinding<'_> {
237 HeaderBinding {
238 salt: &self.salt,
239 kdf_params: &self.kdf_params,
240 nonce_prefix: &self.nonce_prefix,
241 chunk_size: self.chunk_size,
242 metadata_nonce: &self.metadata_nonce,
243 }
244 }
245}
246
247fn read_header_length(bytes: &[u8]) -> Result<u32, HeaderError> {
249 if bytes.len() < 11 {
250 return Err(HeaderError::InsufficientBytes);
251 }
252 let length_bytes = &bytes[7..11];
253 let length = u32::from_le_bytes(
254 length_bytes
255 .try_into()
256 .map_err(|_| HeaderError::InvalidData)?,
257 );
258 Ok(length)
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub enum AadPurpose {
269 Metadata,
270 Content,
271}
272
273impl AadPurpose {
274 fn domain_tag(self) -> &'static [u8] {
275 match self {
276 AadPurpose::Metadata => b"shadow-crypt/v3/metadata",
277 AadPurpose::Content => b"shadow-crypt/v3/content",
278 }
279 }
280}
281
282#[derive(Debug, Clone, Copy)]
296pub struct HeaderBinding<'a> {
297 salt: &'a [u8; 16],
298 kdf_params: &'a KeyDerivationParams,
299 nonce_prefix: &'a [u8; 16],
300 chunk_size: u32,
301 metadata_nonce: &'a [u8; 24],
302}
303
304impl<'a> HeaderBinding<'a> {
305 pub fn new(
306 salt: &'a [u8; 16],
307 kdf_params: &'a KeyDerivationParams,
308 nonce_prefix: &'a [u8; 16],
309 chunk_size: u32,
310 metadata_nonce: &'a [u8; 24],
311 ) -> Self {
312 Self {
313 salt,
314 kdf_params,
315 nonce_prefix,
316 chunk_size,
317 metadata_nonce,
318 }
319 }
320
321 pub fn aad(&self, purpose: AadPurpose) -> Vec<u8> {
324 let mut aad = Vec::with_capacity(FileHeader::min_length() + 24);
325 aad.extend_from_slice(&MAGIC);
326 aad.push(VERSION);
327 aad.extend_from_slice(self.salt);
328 aad.extend_from_slice(&self.kdf_params.memory_cost.to_le_bytes());
329 aad.extend_from_slice(&self.kdf_params.time_cost.to_le_bytes());
330 aad.extend_from_slice(&self.kdf_params.parallelism.to_le_bytes());
331 aad.push(self.kdf_params.key_size);
332 aad.extend_from_slice(self.nonce_prefix);
333 aad.extend_from_slice(&self.chunk_size.to_le_bytes());
334 aad.extend_from_slice(self.metadata_nonce);
335 aad.extend_from_slice(purpose.domain_tag());
336 aad
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::profile;
344
345 fn get_test_params() -> KeyDerivationParams {
346 KeyDerivationParams::from(profile::SecurityProfile::Test)
347 }
348
349 fn create_test_header() -> FileHeader {
350 FileHeader::new(
351 [1u8; 16],
352 get_test_params(),
353 [2u8; 16],
354 1024,
355 [3u8; 24],
356 vec![4, 5, 6, 7, 8],
357 )
358 .unwrap()
359 }
360
361 #[test]
362 fn serialized_magic_and_version_are_correct() {
363 let serialized = create_test_header().serialize();
364 assert_eq!(&serialized[0..6], b"SHADOW");
365 assert_eq!(serialized[6], 3);
366 }
367
368 #[test]
369 fn header_size_is_calculated_correctly() {
370 let header = create_test_header();
371 assert_eq!(header.header_length(), 86 + 5);
372 assert_eq!(header.serialize().len(), header.header_length());
373 }
374
375 #[test]
376 fn oversized_metadata_ciphertext_is_rejected() {
377 let result = FileHeader::new(
378 [0u8; 16],
379 get_test_params(),
380 [0u8; 16],
381 1024,
382 [0u8; 24],
383 vec![0u8; u16::MAX as usize + 1],
384 );
385 assert!(matches!(result, Err(HeaderError::MetadataTooLong)));
386 }
387
388 #[test]
389 fn invalid_chunk_sizes_are_rejected() {
390 for chunk_size in [0, MAX_ACCEPTED_CHUNK_SIZE + 1] {
391 let result = FileHeader::new(
392 [0u8; 16],
393 get_test_params(),
394 [0u8; 16],
395 chunk_size,
396 [0u8; 24],
397 vec![1, 2, 3],
398 );
399 assert!(matches!(result, Err(HeaderError::InvalidData)));
400 }
401 }
402
403 #[test]
404 fn oversized_chunk_size_is_rejected_at_parse_time() {
405 let mut serialized = create_test_header().serialize();
406 serialized[56..60].copy_from_slice(&(MAX_ACCEPTED_CHUNK_SIZE + 1).to_le_bytes());
407 assert!(FileHeader::try_deserialize(&serialized).is_err());
408 }
409
410 #[test]
411 fn aad_differs_by_purpose() {
412 let salt = [1u8; 16];
413 let params = get_test_params();
414 let nonce_prefix = [2u8; 16];
415 let metadata_nonce = [3u8; 24];
416 let binding = HeaderBinding::new(&salt, ¶ms, &nonce_prefix, 1024, &metadata_nonce);
417
418 assert_ne!(
419 binding.aad(AadPurpose::Metadata),
420 binding.aad(AadPurpose::Content)
421 );
422 }
423
424 #[test]
425 fn header_binding_matches_standalone_binding() {
426 let salt = [1u8; 16];
427 let params = get_test_params();
428 let nonce_prefix = [2u8; 16];
429 let metadata_nonce = [3u8; 24];
430
431 let standalone = HeaderBinding::new(&salt, ¶ms, &nonce_prefix, 1024, &metadata_nonce);
432 let header = FileHeader::new(
433 salt,
434 params.clone(),
435 nonce_prefix,
436 1024,
437 metadata_nonce,
438 vec![1, 2, 3],
439 )
440 .unwrap();
441
442 assert_eq!(
443 standalone.aad(AadPurpose::Content),
444 header.binding().aad(AadPurpose::Content)
445 );
446 assert_eq!(
447 standalone.aad(AadPurpose::Metadata),
448 header.binding().aad(AadPurpose::Metadata)
449 );
450 }
451
452 #[test]
453 fn test_round_trip_serialization() {
454 let original = create_test_header();
455 let serialized = original.serialize();
456 assert_eq!(serialized.len(), original.header_length());
457
458 let deserialized = FileHeader::try_deserialize(&serialized).unwrap();
459 assert_eq!(deserialized.salt(), original.salt());
460 assert_eq!(deserialized.kdf_params(), original.kdf_params());
461 assert_eq!(deserialized.nonce_prefix(), original.nonce_prefix());
462 assert_eq!(deserialized.chunk_size(), original.chunk_size());
463 assert_eq!(deserialized.metadata_nonce(), original.metadata_nonce());
464 assert_eq!(
465 deserialized.metadata_ciphertext(),
466 original.metadata_ciphertext()
467 );
468 }
469
470 #[test]
471 fn test_try_deserialize_rejects_wrong_version() {
472 let mut serialized = create_test_header().serialize();
473 serialized[6] = 2; assert!(FileHeader::try_deserialize(&serialized).is_err());
476 }
477
478 #[test]
479 fn test_try_deserialize_rejects_wrong_magic() {
480 let mut serialized = create_test_header().serialize();
481 serialized[0..6].copy_from_slice(b"NOTSHD");
482
483 assert!(FileHeader::try_deserialize(&serialized).is_err());
484 }
485
486 #[test]
487 fn test_try_deserialize_insufficient_bytes() {
488 let bytes = vec![0u8; 50];
489 assert!(matches!(
490 FileHeader::try_deserialize(&bytes),
491 Err(HeaderError::InsufficientBytes)
492 ));
493 }
494
495 #[test]
496 fn test_try_deserialize_inconsistent_lengths() {
497 let mut serialized = create_test_header().serialize();
498 serialized[7..11].copy_from_slice(&(200u32.to_le_bytes()));
500
501 assert!(FileHeader::try_deserialize(&serialized).is_err());
502 }
503}