1use crate::stego::crypto::{NONCE_LEN, SALT_LEN};
44use crate::stego::error::StegoError;
45
46pub const MODE_GHOST: u8 = 0x01;
48pub const MODE_ARMOR: u8 = 0x02;
50
51pub const FRAME_OVERHEAD: usize = 2 + SALT_LEN + NONCE_LEN + 16 + 4; pub const FRAME_OVERHEAD_EXT: usize = 2 + 4 + SALT_LEN + NONCE_LEN + 16 + 4; pub const MAX_FRAME_BYTES: usize = 256 * 1024; pub const MAX_FRAME_BITS: usize = MAX_FRAME_BYTES * 8;
68
69pub fn build_frame(
74 plaintext_len: usize,
75 salt: &[u8; SALT_LEN],
76 nonce: &[u8; NONCE_LEN],
77 ciphertext: &[u8],
78) -> Vec<u8> {
79 debug_assert_eq!(ciphertext.len(), plaintext_len + 16, "ciphertext length mismatch");
80 assert!(plaintext_len <= u32::MAX as usize, "plaintext exceeds u32::MAX");
81
82 let is_v2 = plaintext_len > u16::MAX as usize;
83 let header_len = if is_v2 { 6 } else { 2 };
84 let mut frame = Vec::with_capacity(header_len + SALT_LEN + NONCE_LEN + ciphertext.len() + 4);
85
86 if is_v2 {
87 frame.extend_from_slice(&0u16.to_be_bytes()); frame.extend_from_slice(&(plaintext_len as u32).to_be_bytes());
89 } else {
90 frame.extend_from_slice(&(plaintext_len as u16).to_be_bytes());
91 }
92 frame.extend_from_slice(salt);
93 frame.extend_from_slice(nonce);
94 frame.extend_from_slice(ciphertext);
95
96 let crc = crc32fast::hash(&frame);
97 frame.extend_from_slice(&crc.to_be_bytes());
98
99 frame
100}
101
102pub struct ParsedFrame {
108 pub plaintext_len: u32,
111 pub salt: [u8; SALT_LEN],
113 pub nonce: [u8; NONCE_LEN],
115 pub ciphertext: Vec<u8>,
117}
118
119pub fn parse_frame(data: &[u8]) -> Result<ParsedFrame, StegoError> {
127 if data.len() < 2 {
128 return Err(StegoError::FrameCorrupted);
129 }
130
131 let header_u16 = u16::from_be_bytes([data[0], data[1]]);
133 let (plaintext_len, header_len): (usize, usize) = if header_u16 == 0 && data.len() >= 6 {
134 let v2_len = u32::from_be_bytes([data[2], data[3], data[4], data[5]]) as usize;
135 if v2_len > u16::MAX as usize {
136 (v2_len, 6)
138 } else {
139 (0, 2)
141 }
142 } else {
143 (header_u16 as usize, 2)
144 };
145
146 let ciphertext_len = plaintext_len + 16; let total_frame_len = header_len + SALT_LEN + NONCE_LEN + ciphertext_len + 4;
148
149 if total_frame_len > MAX_FRAME_BYTES || data.len() < total_frame_len {
150 return Err(StegoError::FrameCorrupted);
151 }
152
153 let payload = &data[..total_frame_len - 4];
155 let crc_bytes = &data[total_frame_len - 4..total_frame_len];
156 let stored_crc = u32::from_be_bytes([crc_bytes[0], crc_bytes[1], crc_bytes[2], crc_bytes[3]]);
157 let computed_crc = crc32fast::hash(payload);
158 if stored_crc != computed_crc {
159 return Err(StegoError::FrameCorrupted);
160 }
161
162 let mut salt = [0u8; SALT_LEN];
164 salt.copy_from_slice(&payload[header_len..header_len + SALT_LEN]);
165
166 let mut nonce = [0u8; NONCE_LEN];
167 nonce.copy_from_slice(&payload[header_len + SALT_LEN..header_len + SALT_LEN + NONCE_LEN]);
168
169 let ciphertext = payload[header_len + SALT_LEN + NONCE_LEN..].to_vec();
170
171 Ok(ParsedFrame {
172 plaintext_len: plaintext_len as u32,
173 salt,
174 nonce,
175 ciphertext,
176 })
177}
178
179pub const FORTRESS_COMPACT_FRAME_OVERHEAD: usize = 2 + 16 + 4; pub const FORTRESS_COMPACT_FRAME_OVERHEAD_EXT: usize = 2 + 4 + 16 + 4; pub fn build_fortress_compact_frame(
191 plaintext_len: usize,
192 ciphertext: &[u8],
193) -> Vec<u8> {
194 assert!(plaintext_len <= u32::MAX as usize, "plaintext exceeds u32::MAX");
195
196 let is_v2 = plaintext_len > u16::MAX as usize;
197 let header_len = if is_v2 { 6 } else { 2 };
198 let mut frame = Vec::with_capacity(header_len + ciphertext.len() + 4);
199
200 if is_v2 {
201 frame.extend_from_slice(&0u16.to_be_bytes());
202 frame.extend_from_slice(&(plaintext_len as u32).to_be_bytes());
203 } else {
204 frame.extend_from_slice(&(plaintext_len as u16).to_be_bytes());
205 }
206 frame.extend_from_slice(ciphertext);
207
208 let crc = crc32fast::hash(&frame);
209 frame.extend_from_slice(&crc.to_be_bytes());
210
211 frame
212}
213
214pub fn parse_fortress_compact_frame(data: &[u8]) -> Result<ParsedFrame, StegoError> {
219 use crate::stego::crypto::{FORTRESS_EMPTY_SALT, FORTRESS_EMPTY_NONCE};
220
221 if data.len() < 2 {
222 return Err(StegoError::FrameCorrupted);
223 }
224
225 let header_u16 = u16::from_be_bytes([data[0], data[1]]);
227 let (plaintext_len, header_len): (usize, usize) = if header_u16 == 0 && data.len() >= 6 {
228 let v2_len = u32::from_be_bytes([data[2], data[3], data[4], data[5]]) as usize;
229 if v2_len > u16::MAX as usize {
230 (v2_len, 6)
231 } else {
232 (0, 2)
233 }
234 } else {
235 (header_u16 as usize, 2)
236 };
237
238 let ciphertext_len = plaintext_len + 16;
239 let total_frame_len = header_len + ciphertext_len + 4;
240
241 if total_frame_len > MAX_FRAME_BYTES || data.len() < total_frame_len {
242 return Err(StegoError::FrameCorrupted);
243 }
244
245 let payload = &data[..total_frame_len - 4];
246 let crc_bytes = &data[total_frame_len - 4..total_frame_len];
247 let stored_crc = u32::from_be_bytes([crc_bytes[0], crc_bytes[1], crc_bytes[2], crc_bytes[3]]);
248 let computed_crc = crc32fast::hash(payload);
249 if stored_crc != computed_crc {
250 return Err(StegoError::FrameCorrupted);
251 }
252
253 let ciphertext = payload[header_len..].to_vec();
254
255 Ok(ParsedFrame {
256 plaintext_len: plaintext_len as u32,
257 salt: FORTRESS_EMPTY_SALT,
258 nonce: FORTRESS_EMPTY_NONCE,
259 ciphertext,
260 })
261}
262
263pub fn bytes_to_bits(bytes: &[u8]) -> Vec<u8> {
265 let mut bits = Vec::with_capacity(bytes.len() * 8);
266 for &byte in bytes {
267 for bit_pos in (0..8).rev() {
268 bits.push((byte >> bit_pos) & 1);
269 }
270 }
271 bits
272}
273
274pub fn bits_to_bytes(bits: &[u8]) -> Vec<u8> {
277 let mut bytes = Vec::with_capacity(bits.len().div_ceil(8));
278 for chunk in bits.chunks(8) {
279 let mut byte = 0u8;
280 for (i, &bit) in chunk.iter().enumerate() {
281 byte |= (bit & 1) << (7 - i);
282 }
283 bytes.push(byte);
284 }
285 bytes
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn build_parse_roundtrip() {
294 let salt = [1u8; SALT_LEN];
295 let nonce = [2u8; NONCE_LEN];
296 let ciphertext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
298 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
299 0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB];
300 let frame = build_frame(2, &salt, &nonce, &ciphertext);
301 let parsed = parse_frame(&frame).unwrap();
302
303 assert_eq!(parsed.plaintext_len, 2);
304 assert_eq!(parsed.salt, salt);
305 assert_eq!(parsed.nonce, nonce);
306 assert_eq!(parsed.ciphertext, ciphertext);
307 }
308
309 #[test]
310 fn corrupted_crc_detected() {
311 let salt = [0u8; SALT_LEN];
312 let nonce = [0u8; NONCE_LEN];
313 let ciphertext = vec![0u8; 20];
315 let mut frame = build_frame(4, &salt, &nonce, &ciphertext);
316 let len = frame.len();
318 frame[len - 1] ^= 0xFF;
319 assert!(matches!(parse_frame(&frame), Err(StegoError::FrameCorrupted)));
320 }
321
322 #[test]
323 fn corrupted_length_detected() {
324 let salt = [0u8; SALT_LEN];
325 let nonce = [0u8; NONCE_LEN];
326 let ciphertext = vec![0u8; 20];
328 let mut frame = build_frame(4, &salt, &nonce, &ciphertext);
329 frame[0] = 0xFF;
331 assert!(matches!(parse_frame(&frame), Err(StegoError::FrameCorrupted)));
332 }
333
334 #[test]
335 fn bytes_bits_roundtrip() {
336 let original = vec![0xDE, 0xAD, 0xBE, 0xEF];
337 let bits = bytes_to_bits(&original);
338 assert_eq!(bits.len(), 32);
339 let recovered = bits_to_bytes(&bits);
340 assert_eq!(recovered, original);
341 }
342
343 #[test]
344 fn truncated_data_rejected() {
345 assert!(matches!(parse_frame(&[0x00]), Err(StegoError::FrameCorrupted)));
347 assert!(matches!(parse_frame(&[]), Err(StegoError::FrameCorrupted)));
348 }
349
350 #[test]
351 fn frame_no_mode_byte() {
352 let salt = [3u8; SALT_LEN];
356 let nonce = [4u8; NONCE_LEN];
357 let ciphertext = vec![0x55u8; 20]; let frame = build_frame(4, &salt, &nonce, &ciphertext);
359
360 assert_eq!(frame[0], 0x00);
362 assert_eq!(frame[1], 0x04);
363
364 assert_eq!(frame.len(), 2 + SALT_LEN + NONCE_LEN + 20 + 4);
366
367 let parsed = parse_frame(&frame).unwrap();
369 assert_eq!(parsed.plaintext_len, 4);
370 assert_eq!(parsed.salt, salt);
371 assert_eq!(parsed.nonce, nonce);
372 assert_eq!(parsed.ciphertext, ciphertext);
373 }
374
375 #[test]
376 fn frame_with_zero_length_data() {
377 let salt = [0u8; SALT_LEN];
378 let nonce = [0u8; NONCE_LEN];
379 let ciphertext = vec![0u8; 16];
381 let frame = build_frame(0, &salt, &nonce, &ciphertext);
382 let parsed = parse_frame(&frame).unwrap();
383 assert_eq!(parsed.plaintext_len, 0);
384 assert_eq!(parsed.ciphertext.len(), 16);
385 }
386
387 #[test]
388 fn bits_to_bytes_partial_byte() {
389 let bits = vec![1u8, 0, 1, 1, 0];
391 let bytes = bits_to_bytes(&bits);
392 assert_eq!(bytes.len(), 1);
393 assert_eq!(bytes[0], 0xB0);
395 }
396
397 #[test]
400 fn compact_frame_build_parse_roundtrip() {
401 let ciphertext = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
403 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
404 0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB,
405 0xCC, 0xDD];
406 let frame = build_fortress_compact_frame(4, &ciphertext);
407
408 assert_eq!(frame.len(), 2 + 20 + 4);
410
411 let parsed = parse_fortress_compact_frame(&frame).unwrap();
412 assert_eq!(parsed.plaintext_len, 4);
413 assert_eq!(parsed.ciphertext, ciphertext);
414 assert_eq!(parsed.salt, crate::stego::crypto::FORTRESS_EMPTY_SALT);
416 assert_eq!(parsed.nonce, crate::stego::crypto::FORTRESS_EMPTY_NONCE);
417 }
418
419 #[test]
420 fn compact_frame_smaller_than_full() {
421 let salt = [1u8; SALT_LEN];
423 let nonce = [2u8; NONCE_LEN];
424 let ciphertext = vec![0u8; 20];
425
426 let full_frame = build_frame(4, &salt, &nonce, &ciphertext);
427 let compact_frame = build_fortress_compact_frame(4, &ciphertext);
428
429 assert_eq!(full_frame.len() - compact_frame.len(), SALT_LEN + NONCE_LEN);
432 assert_eq!(full_frame.len() - compact_frame.len(), 28);
433 }
434
435 #[test]
436 fn compact_frame_corrupted_crc_detected() {
437 let ciphertext = vec![0u8; 20];
438 let mut frame = build_fortress_compact_frame(4, &ciphertext);
439 let len = frame.len();
440 frame[len - 1] ^= 0xFF;
441 assert!(matches!(parse_fortress_compact_frame(&frame), Err(StegoError::FrameCorrupted)));
442 }
443
444 #[test]
445 fn compact_frame_truncated_rejected() {
446 assert!(matches!(parse_fortress_compact_frame(&[0x00]), Err(StegoError::FrameCorrupted)));
447 assert!(matches!(parse_fortress_compact_frame(&[]), Err(StegoError::FrameCorrupted)));
448 }
449
450 #[test]
451 fn compact_frame_zero_length() {
452 let ciphertext = vec![0u8; 16];
454 let frame = build_fortress_compact_frame(0, &ciphertext);
455 let parsed = parse_fortress_compact_frame(&frame).unwrap();
456 assert_eq!(parsed.plaintext_len, 0);
457 assert_eq!(parsed.ciphertext.len(), 16);
458 }
459
460 #[test]
461 fn compact_frame_overhead_is_28_less() {
462 assert_eq!(
463 FRAME_OVERHEAD - FORTRESS_COMPACT_FRAME_OVERHEAD,
464 28,
465 "Compact frame saves exactly 28 bytes (salt + nonce)"
466 );
467 }
468
469 #[test]
470 fn v2_ext_overhead_is_4_more() {
471 assert_eq!(FRAME_OVERHEAD_EXT - FRAME_OVERHEAD, 4);
472 assert_eq!(FORTRESS_COMPACT_FRAME_OVERHEAD_EXT - FORTRESS_COMPACT_FRAME_OVERHEAD, 4);
473 }
474
475 #[test]
476 fn v2_frame_build_parse_roundtrip() {
477 let salt = [5u8; SALT_LEN];
478 let nonce = [6u8; NONCE_LEN];
479 let plaintext_len = 70_000usize; let ciphertext = vec![0xAB; plaintext_len + 16];
481 let frame = build_frame(plaintext_len, &salt, &nonce, &ciphertext);
482
483 assert_eq!(frame.len(), FRAME_OVERHEAD_EXT + plaintext_len);
485
486 assert_eq!(frame[0], 0x00);
488 assert_eq!(frame[1], 0x00);
489 assert_eq!(u32::from_be_bytes([frame[2], frame[3], frame[4], frame[5]]), 70_000);
491
492 let parsed = parse_frame(&frame).unwrap();
493 assert_eq!(parsed.plaintext_len, 70_000);
494 assert_eq!(parsed.salt, salt);
495 assert_eq!(parsed.nonce, nonce);
496 assert_eq!(parsed.ciphertext, ciphertext);
497 }
498
499 #[test]
500 fn v1_frame_still_uses_u16_header() {
501 let salt = [7u8; SALT_LEN];
502 let nonce = [8u8; NONCE_LEN];
503 let plaintext_len = 1000usize; let ciphertext = vec![0xCD; plaintext_len + 16];
505 let frame = build_frame(plaintext_len, &salt, &nonce, &ciphertext);
506
507 assert_eq!(frame.len(), FRAME_OVERHEAD + plaintext_len);
509
510 assert_eq!(u16::from_be_bytes([frame[0], frame[1]]), 1000);
512
513 let parsed = parse_frame(&frame).unwrap();
514 assert_eq!(parsed.plaintext_len, 1000);
515 }
516
517 #[test]
518 fn v2_compact_frame_roundtrip() {
519 let plaintext_len = 70_000usize;
520 let ciphertext = vec![0xEF; plaintext_len + 16];
521 let frame = build_fortress_compact_frame(plaintext_len, &ciphertext);
522
523 assert_eq!(frame.len(), FORTRESS_COMPACT_FRAME_OVERHEAD_EXT + plaintext_len);
524
525 let parsed = parse_fortress_compact_frame(&frame).unwrap();
526 assert_eq!(parsed.plaintext_len, 70_000);
527 assert_eq!(parsed.ciphertext, ciphertext);
528 }
529}