Skip to main content

happy_cracking/crypto/
zipcrack.rs

1use anyhow::{Context, Result};
2use clap::Subcommand;
3use rayon::prelude::*;
4use std::io::{Cursor, Read};
5use std::path::PathBuf;
6
7const MAX_BRUTE_SPACE: u128 = 1_000_000_000;
8
9#[derive(Subcommand)]
10pub enum ZipcrackAction {
11    #[command(about = "Dictionary attack against a password-protected zip")]
12    Dict {
13        #[arg(short, long, help = "Path to the encrypted zip file")]
14        file: PathBuf,
15        #[arg(short, long, help = "Path to the wordlist (one password per line)")]
16        wordlist: PathBuf,
17    },
18    #[command(about = "Brute-force attack against a password-protected zip")]
19    Brute {
20        #[arg(short, long, help = "Path to the encrypted zip file")]
21        file: PathBuf,
22        #[arg(
23            short,
24            long,
25            help = "Characters to try in each position",
26            default_value = "abcdefghijklmnopqrstuvwxyz0123456789"
27        )]
28        charset: String,
29        #[arg(long, help = "Minimum password length", default_value = "1")]
30        min_len: usize,
31        #[arg(long, help = "Maximum password length", default_value = "4")]
32        max_len: usize,
33    },
34    #[command(about = "List entries in a zip (name, size, encryption)")]
35    Info {
36        #[arg(short, long, help = "Path to the zip file")]
37        file: PathBuf,
38    },
39}
40
41pub fn run(action: ZipcrackAction) -> Result<()> {
42    match action {
43        ZipcrackAction::Dict { file, wordlist } => {
44            let bytes = std::fs::read(&file)
45                .with_context(|| format!("Failed to read zip file: {}", file.display()))?;
46            let list = std::fs::read_to_string(&wordlist)
47                .with_context(|| format!("Failed to read wordlist: {}", wordlist.display()))?;
48            let words: Vec<&str> = list
49                .lines()
50                .map(str::trim)
51                .filter(|w| !w.is_empty())
52                .collect();
53
54            match dict_attack(&bytes, &words) {
55                Some(password) => println!("Found password: {}", password),
56                None => println!("Not found"),
57            }
58        }
59        ZipcrackAction::Brute {
60            file,
61            charset,
62            min_len,
63            max_len,
64        } => {
65            let bytes = std::fs::read(&file)
66                .with_context(|| format!("Failed to read zip file: {}", file.display()))?;
67
68            match brute_attack(&bytes, &charset, min_len, max_len)? {
69                Some(password) => println!("Found password: {}", password),
70                None => println!("Not found"),
71            }
72        }
73        ZipcrackAction::Info { file } => {
74            let bytes = std::fs::read(&file)
75                .with_context(|| format!("Failed to read zip file: {}", file.display()))?;
76
77            for entry in list_entries(&bytes)? {
78                println!(
79                    "{}  size={}  encrypted={}  method={}",
80                    entry.name, entry.size, entry.encrypted, entry.method
81                );
82            }
83        }
84    }
85    Ok(())
86}
87
88pub struct EntryInfo {
89    pub name: String,
90    pub size: u64,
91    pub encrypted: bool,
92    pub method: String,
93}
94
95pub fn verify_password(zip_bytes: &[u8], password: &str) -> bool {
96    let mut archive = match zip::ZipArchive::new(Cursor::new(zip_bytes)) {
97        Ok(a) => a,
98        Err(_) => return false,
99    };
100
101    let mut saw_encrypted = false;
102    for i in 0..archive.len() {
103        let encrypted = match archive.by_index_raw(i) {
104            Ok(entry) => entry.encrypted(),
105            Err(_) => return false,
106        };
107        if !encrypted {
108            continue;
109        }
110        saw_encrypted = true;
111
112        let mut entry = match archive.by_index_decrypt(i, password.as_bytes()) {
113            Ok(entry) => entry,
114
115            Err(_) => return false,
116        };
117
118        let mut sink = Vec::new();
119        if entry.read_to_end(&mut sink).is_err() {
120            return false;
121        }
122    }
123
124    saw_encrypted
125}
126
127pub fn dict_attack(zip_bytes: &[u8], words: &[&str]) -> Option<String> {
128    words
129        .par_iter()
130        .find_any(|word| verify_password(zip_bytes, word))
131        .map(|word| word.to_string())
132}
133
134pub fn brute_attack(
135    zip_bytes: &[u8],
136    charset: &str,
137    min_len: usize,
138    max_len: usize,
139) -> Result<Option<String>> {
140    let chars: Vec<char> = charset.chars().collect();
141    if chars.is_empty() {
142        anyhow::bail!("Charset must not be empty");
143    }
144    if min_len == 0 {
145        anyhow::bail!("min-len must be at least 1");
146    }
147    if max_len < min_len {
148        anyhow::bail!("max-len ({}) must be >= min-len ({})", max_len, min_len);
149    }
150
151    let base = chars.len() as u128;
152    let mut total: u128 = 0;
153    for len in min_len..=max_len {
154        let count = base.checked_pow(len as u32).unwrap_or(u128::MAX);
155        total = total.saturating_add(count);
156        if total > MAX_BRUTE_SPACE {
157            anyhow::bail!(
158                "Brute-force keyspace ({}+) exceeds the limit of {}",
159                total,
160                MAX_BRUTE_SPACE
161            );
162        }
163    }
164
165    for len in min_len..=max_len {
166        let count = base.pow(len as u32);
167        let found = (0..count).into_par_iter().find_map_any(|index| {
168            let candidate = index_to_candidate(index, &chars, len);
169            if verify_password(zip_bytes, &candidate) {
170                Some(candidate)
171            } else {
172                None
173            }
174        });
175        if found.is_some() {
176            return Ok(found);
177        }
178    }
179
180    Ok(None)
181}
182
183fn index_to_candidate(index: u128, chars: &[char], len: usize) -> String {
184    let base = chars.len() as u128;
185    let mut value = index;
186    let mut out = vec![chars[0]; len];
187    for slot in out.iter_mut().rev() {
188        *slot = chars[(value % base) as usize];
189        value /= base;
190    }
191    out.into_iter().collect()
192}
193
194pub fn list_entries(zip_bytes: &[u8]) -> Result<Vec<EntryInfo>> {
195    let mut archive =
196        zip::ZipArchive::new(Cursor::new(zip_bytes)).context("Failed to open zip archive")?;
197
198    let mut entries = Vec::with_capacity(archive.len());
199    for i in 0..archive.len() {
200        let entry = archive
201            .by_index_raw(i)
202            .with_context(|| format!("Failed to read entry at index {}", i))?;
203        entries.push(EntryInfo {
204            name: entry.name().to_string(),
205            size: entry.size(),
206            encrypted: entry.encrypted(),
207            method: format!("{:?}", entry.compression()),
208        });
209    }
210    Ok(entries)
211}