Skip to main content

happy_cracking/crypto/
autodecode.rs

1use anyhow::Result;
2use clap::Subcommand;
3
4use super::{base32, base58, base64, binary, hex, morse, url};
5
6#[derive(Subcommand)]
7pub enum AutoDecodeAction {
8    #[command(about = "Automatically detect and decode")]
9    Decode {
10        #[arg(help = "Input to decode")]
11        input: String,
12        #[arg(short, long, help = "Recursively decode", default_value = "false")]
13        recursive: bool,
14        #[arg(short, long, help = "Maximum recursion depth", default_value = "5")]
15        max_depth: usize,
16    },
17}
18
19pub fn run(action: AutoDecodeAction) -> Result<()> {
20    match action {
21        AutoDecodeAction::Decode {
22            input,
23            recursive,
24            max_depth,
25        } => {
26            if recursive {
27                decode_recursive(&input, 0, max_depth);
28            } else {
29                let results = detect_and_decode(&input);
30                if results.is_empty() {
31                    println!("No encoding detected");
32                } else {
33                    for (encoding, decoded) in results {
34                        println!("[{}] {}", encoding, decoded);
35                    }
36                }
37            }
38        }
39    }
40    Ok(())
41}
42
43pub fn detect_and_decode(input: &str) -> Vec<(&'static str, String)> {
44    let input = input.trim();
45    let mut results = Vec::new();
46
47    // Try Base64
48    if looks_like_base64(input)
49        && let Ok(decoded) = base64::decode(input)
50        && is_printable(&decoded)
51    {
52        results.push(("Base64", decoded));
53    }
54
55    // Try Base32
56    if looks_like_base32(input)
57        && let Ok(decoded) = base32::decode(input)
58        && is_printable(&decoded)
59    {
60        results.push(("Base32", decoded));
61    }
62
63    // Try Base58
64    if looks_like_base58(input)
65        && let Ok(decoded) = base58::decode(input)
66        && is_printable(&decoded)
67    {
68        results.push(("Base58", decoded));
69    }
70
71    // Try Hex
72    if looks_like_hex(input)
73        && let Ok(decoded) = hex::decode(input)
74        && is_printable(&decoded)
75    {
76        results.push(("Hex", decoded));
77    }
78
79    // Try URL encoding
80    if input.contains('%')
81        && let Ok(decoded) = url::decode(input)
82        && decoded != input
83    {
84        results.push(("URL", decoded));
85    }
86
87    // Try Binary
88    if looks_like_binary(input)
89        && let Ok(decoded) = binary::decode(input)
90        && is_printable(&decoded)
91    {
92        results.push(("Binary", decoded));
93    }
94
95    // Try Morse
96    if looks_like_morse(input)
97        && let Ok(decoded) = morse::decode(input)
98    {
99        results.push(("Morse", decoded));
100    }
101
102    results
103}
104
105fn decode_recursive(input: &str, depth: usize, max_depth: usize) {
106    let indent = "  ".repeat(depth);
107
108    if depth >= max_depth {
109        println!("{}[Max depth reached]", indent);
110        return;
111    }
112
113    let results = detect_and_decode(input);
114
115    if results.is_empty() {
116        if depth == 0 {
117            println!("No encoding detected");
118        }
119        return;
120    }
121
122    for (encoding, decoded) in results {
123        println!("{}[{}] {}", indent, encoding, decoded);
124
125        // Try to decode further
126        if decoded != input && !decoded.is_empty() {
127            decode_recursive(&decoded, depth + 1, max_depth);
128        }
129    }
130}
131
132fn is_printable(s: &str) -> bool {
133    s.chars()
134        .all(|c| !c.is_control() || c == '\n' || c == '\t' || c == '\r')
135}
136
137fn looks_like_base64(s: &str) -> bool {
138    if s.len() < 2 {
139        return false;
140    }
141    let valid_chars = s
142        .chars()
143        .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=');
144    valid_chars && (s.len().is_multiple_of(4) || s.ends_with('='))
145}
146
147fn looks_like_base32(s: &str) -> bool {
148    if s.len() < 2 {
149        return false;
150    }
151    let valid_chars = s
152        .chars()
153        .all(|c| c.is_ascii_alphabetic() || matches!(c, '2'..='7' | '='));
154    valid_chars && s.len().is_multiple_of(8)
155}
156
157fn looks_like_base58(s: &str) -> bool {
158    if s.len() < 2 {
159        return false;
160    }
161    // Base58 excludes 0, O, I, l
162    s.chars()
163        .all(|c| c.is_ascii_alphanumeric() && !matches!(c, '0' | 'O' | 'I' | 'l'))
164}
165
166fn looks_like_hex(s: &str) -> bool {
167    s.len() >= 2 && s.len().is_multiple_of(2) && s.chars().all(|c| c.is_ascii_hexdigit())
168}
169
170fn looks_like_binary(s: &str) -> bool {
171    let non_ws_count = s.chars().filter(|c| !c.is_whitespace()).count();
172    non_ws_count >= 8 && s.chars().all(|c| c == '0' || c == '1' || c.is_whitespace())
173}
174
175fn looks_like_morse(s: &str) -> bool {
176    s.len() >= 2
177        && s.chars()
178            .all(|c| c == '.' || c == '-' || c == ' ' || c == '/')
179}