aescrypt_rs/
header.rs

1//! src/header.rs
2//! Quick AES Crypt version detection — works with real v0–v3 files
3
4use crate::error::AescryptError;
5use std::io::Read;
6
7/// Reads just enough of the file to determine the AES Crypt version (0–3).
8///
9/// This function is deliberately lenient with v0 files (3-byte or 4-byte headers)
10/// because that's how the original AES Crypt tools behaved.
11///
12/// Returns `Ok(version)` or an appropriate error.
13pub fn read_version<R: Read>(mut reader: R) -> Result<u8, AescryptError> {
14    let mut magic = [0u8; 3];
15    reader.read_exact(&mut magic).map_err(AescryptError::Io)?;
16
17    if magic != [b'A', b'E', b'S'] {
18        return Err(AescryptError::Header(
19            "Not an AES Crypt file: invalid magic".into(),
20        ));
21    }
22
23    let mut buf = [0u8; 2];
24    match reader.read(&mut buf) {
25        Ok(2) => {
26            let version = buf[0];
27            let reserved = buf[1];
28
29            if version > 3 {
30                return Err(AescryptError::Header(format!(
31                    "Unsupported version: {version}"
32                )));
33            }
34            if version >= 1 && reserved != 0x00 {
35                return Err(AescryptError::Header(
36                    "Invalid header: reserved byte != 0x00".into(),
37                ));
38            }
39            Ok(version)
40        }
41        Ok(1) => {
42            // Only one byte after "AES" → legacy v0 4-byte header
43            if buf[0] == 0 {
44                Ok(0)
45            } else {
46                Err(AescryptError::Header(
47                    "Invalid v0 header: version byte not zero".into(),
48                ))
49            }
50        }
51        Ok(0) | Err(_) => {
52            // EOF after "AES" → classic 3-byte v0 header
53            Ok(0)
54        }
55        _ => unreachable!("read() only returns 0–2 for a 2-byte buffer"),
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use std::io::Cursor;
63
64    #[test]
65    fn real_world_vectors() {
66        let cases = &[
67            ("41455300", 0u8),   // v0: 4-byte
68            ("4145530000", 0u8), // v0: 5-byte clean
69            ("41455300ff", 0u8), // v0: 5-byte garbage
70            ("4145530100", 1u8),
71            ("4145530200", 2u8),
72            ("4145530300", 3u8),
73        ];
74
75        for &(hex, expected) in cases {
76            let bytes = hex::decode(hex).unwrap();
77            assert_eq!(read_version(Cursor::new(&bytes)).unwrap(), expected);
78        }
79    }
80
81    #[test]
82    fn short_file_only_aes() {
83        assert_eq!(read_version(Cursor::new(b"AES")).unwrap(), 0);
84    }
85
86    #[test]
87    fn invalid_magic() {
88        let err = read_version(Cursor::new(b"XYZ\x03\x00")).unwrap_err();
89        assert_eq!(
90            err.to_string(),
91            "Header error: Not an AES Crypt file: invalid magic"
92        );
93    }
94}