1use crate::{errors::HeaderError, v2::key::KeyDerivationParams};
2
3#[derive(Debug, Clone)]
9pub struct FileHeader {
10 pub magic: [u8; 6], pub version: u8, pub header_length: u32, pub salt: [u8; 16], pub kdf_memory: u32, pub kdf_iterations: u32, pub kdf_parallelism: u32, pub kdf_key_length: u8, pub content_nonce: [u8; 24], pub filename_nonce: [u8; 24], pub filename_ciphertext_length: u16, pub filename_ciphertext: Vec<u8>, }
23
24pub const MAGIC: [u8; 6] = *b"SHADOW";
25pub const VERSION: u8 = 2;
26
27impl FileHeader {
28 pub fn new(
32 salt: [u8; 16],
33 kdf_params: KeyDerivationParams,
34 content_nonce: [u8; 24],
35 filename_nonce: [u8; 24],
36 filename_ciphertext: Vec<u8>,
37 ) -> Result<Self, HeaderError> {
38 let filename_ciphertext_length: u16 = filename_ciphertext
39 .len()
40 .try_into()
41 .map_err(|_| HeaderError::FilenameTooLong)?;
42 let size = Self::min_length() + filename_ciphertext.len();
43
44 Ok(FileHeader {
45 magic: MAGIC,
46 version: VERSION,
47 header_length: size as u32,
48 salt,
49 kdf_memory: kdf_params.memory_cost,
50 kdf_iterations: kdf_params.time_cost,
51 kdf_parallelism: kdf_params.parallelism,
52 kdf_key_length: kdf_params.key_size,
53 content_nonce,
54 filename_nonce,
55 filename_ciphertext_length,
56 filename_ciphertext,
57 })
58 }
59
60 pub fn min_length() -> usize {
63 6 + 1 + 4 + 16 + 4 + 4 + 4 + 1 + 24 + 24 + 2 }
75
76 pub fn binding(&self) -> HeaderBinding<'_> {
79 HeaderBinding {
80 salt: &self.salt,
81 kdf_memory: self.kdf_memory,
82 kdf_iterations: self.kdf_iterations,
83 kdf_parallelism: self.kdf_parallelism,
84 kdf_key_length: self.kdf_key_length,
85 content_nonce: &self.content_nonce,
86 filename_nonce: &self.filename_nonce,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum AadPurpose {
98 Filename,
99 Content,
100}
101
102impl AadPurpose {
103 fn domain_tag(self) -> &'static [u8] {
104 match self {
105 AadPurpose::Filename => b"shadow-crypt/v2/filename",
106 AadPurpose::Content => b"shadow-crypt/v2/content",
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy)]
125pub struct HeaderBinding<'a> {
126 pub salt: &'a [u8; 16],
127 pub kdf_memory: u32,
128 pub kdf_iterations: u32,
129 pub kdf_parallelism: u32,
130 pub kdf_key_length: u8,
131 pub content_nonce: &'a [u8; 24],
132 pub filename_nonce: &'a [u8; 24],
133}
134
135impl<'a> HeaderBinding<'a> {
136 pub fn new(
137 salt: &'a [u8; 16],
138 kdf_params: &KeyDerivationParams,
139 content_nonce: &'a [u8; 24],
140 filename_nonce: &'a [u8; 24],
141 ) -> Self {
142 Self {
143 salt,
144 kdf_memory: kdf_params.memory_cost,
145 kdf_iterations: kdf_params.time_cost,
146 kdf_parallelism: kdf_params.parallelism,
147 kdf_key_length: kdf_params.key_size,
148 content_nonce,
149 filename_nonce,
150 }
151 }
152
153 pub fn aad(&self, purpose: AadPurpose) -> Vec<u8> {
156 let mut aad = Vec::with_capacity(FileHeader::min_length() + 24);
157 aad.extend_from_slice(&MAGIC);
158 aad.push(VERSION);
159 aad.extend_from_slice(self.salt);
160 aad.extend_from_slice(&self.kdf_memory.to_le_bytes());
161 aad.extend_from_slice(&self.kdf_iterations.to_le_bytes());
162 aad.extend_from_slice(&self.kdf_parallelism.to_le_bytes());
163 aad.push(self.kdf_key_length);
164 aad.extend_from_slice(self.content_nonce);
165 aad.extend_from_slice(self.filename_nonce);
166 aad.extend_from_slice(purpose.domain_tag());
167 aad
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::profile;
175
176 fn get_test_params() -> KeyDerivationParams {
177 KeyDerivationParams::from(profile::SecurityProfile::Test)
178 }
179
180 #[test]
181 fn default_values_are_correct() {
182 let header = FileHeader::new(
183 [0u8; 16],
184 get_test_params(),
185 [0u8; 24],
186 [0u8; 24],
187 vec![1, 2, 3, 4],
188 )
189 .unwrap();
190
191 assert_eq!(&header.magic, b"SHADOW");
192 assert_eq!(header.version, 2);
193 }
194
195 #[test]
196 fn header_size_is_calculated_correctly() {
197 let filename_ciphertext = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
198 let header = FileHeader::new(
199 [0u8; 16],
200 get_test_params(),
201 [0u8; 24],
202 [0u8; 24],
203 filename_ciphertext.clone(),
204 )
205 .unwrap();
206
207 let expected_size: u32 = 90 + filename_ciphertext.len() as u32;
208 assert_eq!(header.header_length, expected_size);
209 }
210
211 #[test]
212 fn oversized_filename_ciphertext_is_rejected() {
213 let filename_ciphertext = vec![0u8; u16::MAX as usize + 1];
214 let result = FileHeader::new(
215 [0u8; 16],
216 get_test_params(),
217 [0u8; 24],
218 [0u8; 24],
219 filename_ciphertext,
220 );
221 assert!(matches!(result, Err(HeaderError::FilenameTooLong)));
222 }
223
224 #[test]
225 fn max_length_filename_ciphertext_is_accepted() {
226 let filename_ciphertext = vec![0u8; u16::MAX as usize];
227 let header = FileHeader::new(
228 [0u8; 16],
229 get_test_params(),
230 [0u8; 24],
231 [0u8; 24],
232 filename_ciphertext,
233 )
234 .unwrap();
235 assert_eq!(header.filename_ciphertext_length, u16::MAX);
236 }
237
238 #[test]
239 fn aad_differs_by_purpose() {
240 let salt = [1u8; 16];
241 let params = get_test_params();
242 let content_nonce = [2u8; 24];
243 let filename_nonce = [3u8; 24];
244 let binding = HeaderBinding::new(&salt, ¶ms, &content_nonce, &filename_nonce);
245
246 assert_ne!(
247 binding.aad(AadPurpose::Filename),
248 binding.aad(AadPurpose::Content)
249 );
250 }
251
252 #[test]
253 fn header_binding_matches_standalone_binding() {
254 let salt = [1u8; 16];
255 let params = get_test_params();
256 let content_nonce = [2u8; 24];
257 let filename_nonce = [3u8; 24];
258
259 let standalone = HeaderBinding::new(&salt, ¶ms, &content_nonce, &filename_nonce);
260 let header =
261 FileHeader::new(salt, params, content_nonce, filename_nonce, vec![1, 2, 3]).unwrap();
262
263 assert_eq!(
264 standalone.aad(AadPurpose::Content),
265 header.binding().aad(AadPurpose::Content)
266 );
267 assert_eq!(
268 standalone.aad(AadPurpose::Filename),
269 header.binding().aad(AadPurpose::Filename)
270 );
271 }
272}