happy_cracking/crypto/
zipcrack.rs1use 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}