zip_blitz/
lib.rs

1use clap::Parser;
2use std::error::Error;
3use std::fmt;
4use std::fs::File;
5use std::io::Read;
6use zip::read::ZipFile;
7
8#[derive(Debug)]
9struct ZipFileConfigError(String);
10
11impl fmt::Display for ZipFileConfigError {
12    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13        write!(f, "ZipFileConfigError: {}", self.0)
14    }
15}
16
17impl Error for ZipFileConfigError {}
18
19#[derive(Parser, Debug)]
20#[clap(author, version, about, long_about = None)]
21pub struct Args {
22    /// Name of the zip file
23    #[clap(short, long)]
24    zip_name: String,
25
26    /// Name of the file to test extraction with
27    #[clap(short, long)]
28    file_name: String,
29
30    /// File extension
31    #[clap(short = 't', long)]
32    file_extension: Option<String>,
33}
34
35pub struct Config {
36    pub archive: zip::ZipArchive<File>,
37    pub file_name: String,
38    pub file_header: Option<Vec<u8>>,
39}
40
41impl Config {
42    pub fn new(args: Args) -> Result<Config, Box<dyn Error>> {
43        let zip_path = std::path::Path::new(&args.zip_name);
44        let zip_file = std::fs::File::open(&zip_path)?;
45        let file_extension = match &args.file_extension {
46            Some(file_extension) => file_extension.to_owned(),
47            None => guess_file_type(&args.file_name)?,
48        };
49        let mut archive = zip::ZipArchive::new(zip_file)?;
50        let file_exists = check_if_file_exists_in_zip(&mut archive, &args.file_name);
51        if file_exists {
52            Ok(Config {
53                archive,
54                file_name: args.file_name,
55                file_header: get_header(&file_extension),
56            })
57        } else {
58            Err(Box::new(ZipFileConfigError(
59                "File does not exist in zip".into(),
60            )))
61        }
62    }
63}
64
65pub fn run<R>(mut config: Config, mut passwords: R) -> Result<String, &'static str>
66where
67    R: Iterator<Item = String>,
68{
69    let header = &config.file_header;
70    if let Some(header) = header {
71        passwords
72            .find(|p| {
73                config
74                    .archive
75                    .by_name_decrypt(&config.file_name, p.as_bytes())
76                    .ok()
77                    .and_then(|r| r.into())
78                    .map_or(false, |mut file| {
79                        is_header_valid(&mut file, header.to_vec())
80                    })
81            })
82            .ok_or("Password wasn't found")
83    } else {
84        return Err("Unable to detect file header");
85    }
86}
87
88fn is_header_valid(file: &mut ZipFile, file_header: Vec<u8>) -> bool {
89    let mut actual_header = [0u8; 128];
90    let header = &mut actual_header[..file_header.len()];
91    file.read_exact(header).is_ok() && header == file_header
92}
93
94pub fn get_header(extension: &str) -> Option<Vec<u8>> {
95    match extension {
96        "asf" | "wma" | "wmv" => Some(vec![
97            0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62,
98            0xCE, 0x6C,
99        ]),
100        "png" => Some(vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
101        "jpg" => Some(vec![0xFF, 0xD8]),
102        "zip" | "apk" | "jar" => Some(vec![0x50, 0x4B, 0x03, 0x04]),
103        "xml" => Some(vec![0x3C, 0x3F, 0x78, 0x6D, 0x6C, 0x20]),
104        _ => None,
105    }
106}
107
108fn guess_file_type(file_name: &str) -> Result<String, &'static str> {
109    let ext = file_name
110        .split('.')
111        .next_back()
112        .and_then(|ext| Some(ext.to_string()));
113    match ext {
114        Some(ext) => Ok(ext),
115        None => Err("failed to guess file type"),
116    }
117}
118
119fn check_if_file_exists_in_zip(archive: &mut zip::ZipArchive<File>, file_name: &str) -> bool {
120    let mut file_names = archive.file_names();
121    let file_exits = file_names.find(|&x| x == file_name);
122    match file_exits {
123        Some(_) => true,
124        None => false,
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::path::PathBuf;
132
133    #[test]
134    fn test_is_header_valid() {
135        let mut test_zip_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
136        test_zip_path.push("test_data/cats.zip");
137        let zip_file = std::fs::File::open(&test_zip_path).unwrap();
138        let mut archive = zip::ZipArchive::new(&zip_file).unwrap();
139        let mut file = archive.by_name_decrypt("kitten.jpg", b"fun").unwrap();
140        assert_eq!(true, is_header_valid(&mut file, get_header("jpg").unwrap()));
141    }
142
143    #[test]
144    fn test_blitz_zip_with_jpg() {
145        let mut test_zip_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
146        test_zip_path.push("test_data/cats.zip");
147        let config = Config::new(Args {
148            zip_name: test_zip_path.into_os_string().into_string().unwrap(),
149            file_name: String::from("kitten.jpg"),
150            file_extension: Some(String::from("jpg")),
151        })
152        .unwrap();
153        let wordlist = std::fs::read_to_string("test_data/wordlist.txt")
154            .expect("Something went wrong reading the file");
155        let lines = wordlist.lines().map(|x| x.to_string());
156        if let Ok(password) = run(config, lines) {
157            assert_eq!(password, "fun");
158        } else {
159            panic!("password validation logic faild");
160        }
161    }
162
163    #[test]
164    fn test_blitz_zip_without_type_provided() {
165        let mut test_zip_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
166        test_zip_path.push("test_data/cats.zip");
167        let config = Config::new(Args {
168            zip_name: test_zip_path.into_os_string().into_string().unwrap(),
169            file_name: String::from("kitten.jpg"),
170            file_extension: None,
171        })
172        .unwrap();
173        let wordlist = std::fs::read_to_string("test_data/wordlist.txt")
174            .expect("Something went wrong reading the file");
175        let lines = wordlist.lines().map(|x| x.to_string());
176        if let Ok(password) = run(config, lines) {
177            assert_eq!(password, "fun");
178        } else {
179            panic!("password validation logic faild");
180        }
181    }
182}