Skip to main content

shadow_crypt_core/v3/
header.rs

1use crate::{
2    errors::{FileError, HeaderError},
3    file::FileMetadata,
4    memory::SecureKey,
5    v3::{crypt, key::KeyDerivationParams, metadata},
6};
7
8/// Complete v3 file header.
9///
10/// Differences from v2: the content is encrypted as a sequence of AEAD
11/// chunks (see [`crate::v3::stream`]) using a 16-byte nonce prefix stored
12/// here, and the bare filename ciphertext is replaced by an encrypted
13/// metadata envelope carrying the filename plus optional mtime and Unix
14/// mode. All fixed fields are authenticated as associated data via
15/// [`HeaderBinding`], with distinct domains for metadata and content.
16///
17/// The struct holds only the header's actual information content; the layout
18/// artifacts of the serialized form (magic, version byte, length fields) are
19/// computed during (de)serialization and never stored.
20///
21/// Serialized layout:
22///
23/// | field                      | size     |
24/// |----------------------------|----------|
25/// | magic ("SHADOW")           | 6 bytes  |
26/// | version (3)                | 1 byte   |
27/// | header_length              | 4 bytes  |
28/// | salt                       | 16 bytes |
29/// | kdf_memory                 | 4 bytes  |
30/// | kdf_iterations             | 4 bytes  |
31/// | kdf_parallelism            | 4 bytes  |
32/// | kdf_key_length             | 1 byte   |
33/// | nonce_prefix               | 16 bytes |
34/// | chunk_size                 | 4 bytes  |
35/// | metadata_nonce             | 24 bytes |
36/// | metadata_ciphertext_length | 2 bytes  |
37/// | metadata_ciphertext        | variable |
38#[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
51/// Largest chunk size accepted when parsing a header. Bounds the per-chunk
52/// allocation a crafted file can request.
53pub const MAX_ACCEPTED_CHUNK_SIZE: u32 = 64 * 1024 * 1024; // 64 MiB
54
55impl FileHeader {
56    /// Builds a v3 header. Fails with [`HeaderError::MetadataTooLong`] if the
57    /// metadata ciphertext does not fit the u16 length field, and with
58    /// [`HeaderError::InvalidData`] for a chunk size outside
59    /// `1..=MAX_ACCEPTED_CHUNK_SIZE`.
60    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    /// Minimum length of the serialized header without the variable-length
86    /// metadata ciphertext. Changing the layout requires updating this value.
87    pub(crate) const fn min_length() -> usize {
88        6  // magic ("SHADOW")
89        + 1  // version (u8)
90        + 4  // header_length (u32)
91        + 16 // salt ([u8; 16])
92        + 4  // kdf_memory (u32)
93        + 4  // kdf_iterations (u32)
94        + 4  // kdf_parallelism (u32)
95        + 1  // kdf_key_length (u8)
96        + 16 // nonce_prefix ([u8; 16])
97        + 4  // chunk_size (u32)
98        + 24 // metadata_nonce ([u8; 24])
99        + 2 // metadata_ciphertext_length (u16)
100    }
101
102    /// Total length of this header's serialized form.
103    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    /// The key derivation parameters recorded in this header.
202    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    /// Decrypts and parses the metadata envelope stored in this header,
223    /// verifying the header binding under the metadata domain.
224    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    /// The header binding used as associated data for this header's AEAD
235    /// operations.
236    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
247/// Reads the total header length out of the fixed header fields.
248fn 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/// Which ciphertext an AEAD operation belongs to.
262///
263/// The purpose is mixed into the associated data, giving the metadata
264/// envelope and the content chunks distinct domains: a ciphertext produced
265/// for one purpose can never authenticate for the other, even under the
266/// same key.
267#[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/// The fixed header fields bound as associated data to every v3 AEAD
283/// operation.
284///
285/// This exists separately from [`FileHeader`] because on encryption the
286/// associated data is needed *before* the header can be built (the header
287/// contains the metadata ciphertext, which is itself AEAD-encrypted under
288/// this binding).
289///
290/// The variable-length fields (`header_length`, `metadata_ciphertext_length`,
291/// and the metadata ciphertext itself) are deliberately excluded: they depend
292/// on the metadata encryption output, and tampering with them is already
293/// detected — a shifted length changes which bytes are interpreted as
294/// ciphertext, which fails authentication.
295#[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    /// Serializes the binding into the associated data for one AEAD
322    /// operation. Field order mirrors the header serialization.
323    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, &params, &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, &params, &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; // claim v2
474
475        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        // header_length no longer matches min_length + metadata_ciphertext_length
499        serialized[7..11].copy_from_slice(&(200u32.to_le_bytes()));
500
501        assert!(FileHeader::try_deserialize(&serialized).is_err());
502    }
503}