1use sha2::{Digest, Sha256};
33
34use crate::crypto::{FipsCrypto, SecureKey, NONCE_SIZE};
35use crate::error::{Result, VaultError};
36
37pub const DEFAULT_CHUNK_SIZE: usize = 4 * 1024 * 1024;
39
40pub const STREAM_MAGIC: &[u8; 4] = b"AIMV";
42
43pub const STREAM_VERSION: u8 = 1;
45
46pub const HEADER_SIZE: usize = 32;
48
49#[derive(Debug, Clone)]
51pub struct StreamHeader {
52 pub version: u8,
53 pub chunk_size: u32,
54 pub total_chunks: u64,
55 pub original_size: u64,
56}
57
58impl StreamHeader {
59 pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
61 let mut buf = [0u8; HEADER_SIZE];
62 buf[0..4].copy_from_slice(STREAM_MAGIC);
63 buf[4] = self.version;
64 buf[5..9].copy_from_slice(&self.chunk_size.to_le_bytes());
65 buf[9..17].copy_from_slice(&self.total_chunks.to_le_bytes());
66 buf[17..25].copy_from_slice(&self.original_size.to_le_bytes());
67 buf
69 }
70
71 pub fn from_bytes(buf: &[u8]) -> Result<Self> {
73 if buf.len() < HEADER_SIZE {
74 return Err(VaultError::CryptoError(format!(
75 "Stream header too short: {} < {}",
76 buf.len(),
77 HEADER_SIZE
78 )));
79 }
80
81 if &buf[0..4] != STREAM_MAGIC {
82 return Err(VaultError::CryptoError(
83 "Invalid stream magic bytes — not an AIMV chunked file".to_string(),
84 ));
85 }
86
87 let version = buf[4];
88 if version != STREAM_VERSION {
89 return Err(VaultError::CryptoError(format!(
90 "Unsupported stream version: {} (expected {})",
91 version, STREAM_VERSION
92 )));
93 }
94
95 let chunk_size = u32::from_le_bytes(buf[5..9].try_into().unwrap());
96 let total_chunks = u64::from_le_bytes(buf[9..17].try_into().unwrap());
97 let original_size = u64::from_le_bytes(buf[17..25].try_into().unwrap());
98
99 Ok(Self {
100 version,
101 chunk_size,
102 total_chunks,
103 original_size,
104 })
105 }
106}
107
108pub fn encrypt_chunked(
113 crypto: &FipsCrypto,
114 data: &[u8],
115 key: &SecureKey,
116 chunk_size: usize,
117) -> Result<Vec<u8>> {
118 let chunk_size = if chunk_size == 0 {
119 DEFAULT_CHUNK_SIZE
120 } else {
121 chunk_size
122 };
123 let total_chunks = data.len().div_ceil(chunk_size);
124
125 let header = StreamHeader {
127 version: STREAM_VERSION,
128 chunk_size: chunk_size as u32,
129 total_chunks: total_chunks as u64,
130 original_size: data.len() as u64,
131 };
132
133 let mut output = Vec::new();
134 output.extend_from_slice(&header.to_bytes());
135
136 let mut mac_hasher = Sha256::new();
138
139 for (i, chunk) in data.chunks(chunk_size).enumerate() {
140 let encrypted_chunk = crypto.encrypt(chunk, key)?;
141
142 if encrypted_chunk.len() > NONCE_SIZE {
145 let tag_start = encrypted_chunk.len().saturating_sub(16);
146 mac_hasher.update(&encrypted_chunk[tag_start..]);
147 }
148
149 output.extend_from_slice(&encrypted_chunk);
150 let _ = i; }
152
153 mac_hasher.update((total_chunks as u64).to_le_bytes());
155 let stream_mac = mac_hasher.finalize();
156 output.extend_from_slice(&stream_mac);
157
158 Ok(output)
159}
160
161pub fn decrypt_chunked(crypto: &FipsCrypto, encrypted: &[u8], key: &SecureKey) -> Result<Vec<u8>> {
165 if encrypted.len() < HEADER_SIZE + 32 {
166 return Err(VaultError::CryptoError(
167 "Encrypted data too short for chunked format".to_string(),
168 ));
169 }
170
171 let header = StreamHeader::from_bytes(&encrypted[..HEADER_SIZE])?;
172 let chunk_size = header.chunk_size as usize;
173
174 let chunk_overhead = NONCE_SIZE + 16;
177
178 let body = &encrypted[HEADER_SIZE..encrypted.len() - 32]; let stored_mac = &encrypted[encrypted.len() - 32..];
180
181 let mut output = Vec::with_capacity(header.original_size as usize);
182 let mut mac_hasher = Sha256::new();
183 let mut offset = 0;
184 let mut chunks_decrypted = 0u64;
185
186 while offset < body.len() {
187 let remaining_plaintext = (header.original_size as usize).saturating_sub(output.len());
189 let this_chunk_plain_size = remaining_plaintext.min(chunk_size);
190 let this_chunk_enc_size = this_chunk_plain_size + chunk_overhead;
191
192 if offset + this_chunk_enc_size > body.len() {
193 return Err(VaultError::CryptoError(format!(
194 "Truncated chunk at offset {}: need {} bytes, have {}",
195 offset,
196 this_chunk_enc_size,
197 body.len() - offset
198 )));
199 }
200
201 let chunk_data = &body[offset..offset + this_chunk_enc_size];
202
203 let tag_start = chunk_data.len().saturating_sub(16);
205 mac_hasher.update(&chunk_data[tag_start..]);
206
207 let decrypted = crypto.decrypt(chunk_data, key)?;
208 output.extend_from_slice(&decrypted);
209
210 offset += this_chunk_enc_size;
211 chunks_decrypted += 1;
212 }
213
214 mac_hasher.update(chunks_decrypted.to_le_bytes());
216 let computed_mac = mac_hasher.finalize();
217
218 if &computed_mac[..] != stored_mac {
219 return Err(VaultError::IntegrityError(
220 "Stream MAC verification failed — data may have been truncated, reordered, or tampered with".to_string(),
221 ));
222 }
223
224 if chunks_decrypted != header.total_chunks {
225 return Err(VaultError::IntegrityError(format!(
226 "Expected {} chunks, got {}",
227 header.total_chunks, chunks_decrypted
228 )));
229 }
230
231 Ok(output)
232}
233
234pub fn is_chunked_format(data: &[u8]) -> bool {
236 data.len() >= HEADER_SIZE && &data[0..4] == STREAM_MAGIC
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_header_roundtrip() {
245 let header = StreamHeader {
246 version: STREAM_VERSION,
247 chunk_size: 4096,
248 total_chunks: 10,
249 original_size: 40960,
250 };
251 let bytes = header.to_bytes();
252 let parsed = StreamHeader::from_bytes(&bytes).unwrap();
253 assert_eq!(parsed.version, header.version);
254 assert_eq!(parsed.chunk_size, header.chunk_size);
255 assert_eq!(parsed.total_chunks, header.total_chunks);
256 assert_eq!(parsed.original_size, header.original_size);
257 }
258
259 #[test]
260 fn test_encrypt_decrypt_chunked_small() {
261 let crypto = FipsCrypto::new().unwrap();
262 let passphrase = b"test_passphrase_with_sufficient_entropy".to_vec();
263 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
264
265 let data = b"Hello, AI Model Vault streaming encryption!";
266 let encrypted = encrypt_chunked(&crypto, data, &key, 16).unwrap();
267
268 assert!(is_chunked_format(&encrypted));
269
270 let decrypted = decrypt_chunked(&crypto, &encrypted, &key).unwrap();
271 assert_eq!(data.as_slice(), &decrypted[..]);
272 }
273
274 #[test]
275 fn test_encrypt_decrypt_chunked_exact_boundary() {
276 let crypto = FipsCrypto::new().unwrap();
277 let passphrase = b"test_passphrase_boundary".to_vec();
278 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
279
280 let data = vec![0xABu8; 96];
282 let encrypted = encrypt_chunked(&crypto, &data, &key, 32).unwrap();
283 let decrypted = decrypt_chunked(&crypto, &encrypted, &key).unwrap();
284 assert_eq!(data, decrypted);
285 }
286
287 #[test]
288 fn test_encrypt_decrypt_chunked_large() {
289 let crypto = FipsCrypto::new().unwrap();
290 let passphrase = b"test_passphrase_large_data".to_vec();
291 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
292
293 let data: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
295 let encrypted = encrypt_chunked(&crypto, &data, &key, 64 * 1024).unwrap();
296 let decrypted = decrypt_chunked(&crypto, &encrypted, &key).unwrap();
297 assert_eq!(data, decrypted);
298 }
299
300 #[test]
301 fn test_tampered_stream_mac_fails() {
302 let crypto = FipsCrypto::new().unwrap();
303 let passphrase = b"test_passphrase_tamper".to_vec();
304 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
305
306 let data = b"Secret model data";
307 let mut encrypted = encrypt_chunked(&crypto, data, &key, 8).unwrap();
308
309 let len = encrypted.len();
311 encrypted[len - 1] ^= 0xFF;
312
313 let result = decrypt_chunked(&crypto, &encrypted, &key);
314 assert!(result.is_err());
315 }
316
317 #[test]
318 fn test_invalid_magic_bytes_rejected() {
319 let bad_data = vec![0u8; 100];
320 assert!(!is_chunked_format(&bad_data));
321 let result = StreamHeader::from_bytes(&bad_data);
322 assert!(result.is_err());
323 }
324
325 #[test]
326 fn test_empty_data() {
327 let crypto = FipsCrypto::new().unwrap();
328 let passphrase = b"test_passphrase_empty".to_vec();
329 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
330
331 let data = b"";
332 let encrypted = encrypt_chunked(&crypto, data, &key, 32).unwrap();
333 let decrypted = decrypt_chunked(&crypto, &encrypted, &key).unwrap();
334 assert_eq!(data.as_slice(), &decrypted[..]);
335 }
336
337 #[test]
338 fn test_truncated_chunk_rejected() {
339 let crypto = FipsCrypto::new().unwrap();
340 let passphrase = b"truncation_test".to_vec();
341 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
342
343 let data = vec![0xCC; 64];
344 let mut encrypted = encrypt_chunked(&crypto, &data, &key, 16).unwrap();
345
346 let mac = encrypted[encrypted.len() - 32..].to_vec();
349 encrypted.truncate(encrypted.len() - 32 - 5);
350 encrypted.extend_from_slice(&mac);
351
352 let result = decrypt_chunked(&crypto, &encrypted, &key);
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_single_byte_data() {
358 let crypto = FipsCrypto::new().unwrap();
359 let passphrase = b"single_byte_test".to_vec();
360 let (key, _) = crypto.derive_key(passphrase, None).unwrap();
361
362 let data = vec![0x42];
363 let encrypted = encrypt_chunked(&crypto, &data, &key, 1).unwrap();
364 let decrypted = decrypt_chunked(&crypto, &encrypted, &key).unwrap();
365 assert_eq!(data, decrypted);
366 }
367
368 #[test]
369 fn test_from_bytes_too_short() {
370 let short = vec![0u8; 10]; let result = StreamHeader::from_bytes(&short);
373 assert!(result.is_err());
374 let err_msg = format!("{}", result.err().unwrap());
375 assert!(err_msg.contains("too short"));
376 }
377
378 #[test]
379 fn test_from_bytes_bad_version() {
380 let mut header_bytes = StreamHeader {
382 version: 1,
383 chunk_size: 1024,
384 total_chunks: 1,
385 original_size: 100,
386 }
387 .to_bytes();
388 header_bytes[4] = 99; let result = StreamHeader::from_bytes(&header_bytes);
390 assert!(result.is_err());
391 let err_msg = format!("{}", result.err().unwrap());
392 assert!(err_msg.contains("Unsupported stream version"));
393 }
394
395 #[test]
396 fn test_decrypt_chunked_too_short() {
397 let crypto = FipsCrypto::new().unwrap();
399 let (key, _) = crypto.derive_key(b"short_test".to_vec(), None).unwrap();
400 let short = vec![0u8; 10]; let result = decrypt_chunked(&crypto, &short, &key);
402 assert!(result.is_err());
403 let err_msg = format!("{}", result.err().unwrap());
404 assert!(err_msg.contains("too short"));
405 }
406}