Skip to main content

happy_cracking/crypto/
jwt.rs

1use anyhow::{Context, Result};
2use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
3use clap::Subcommand;
4
5#[derive(Subcommand)]
6pub enum JwtAction {
7    #[command(about = "Decode a JWT token (without verification)")]
8    Decode {
9        #[arg(help = "JWT token string")]
10        token: String,
11    },
12    #[command(about = "Analyze a JWT token for potential vulnerabilities")]
13    Analyze {
14        #[arg(help = "JWT token string")]
15        token: String,
16    },
17}
18
19pub fn run(action: JwtAction) -> Result<()> {
20    match action {
21        JwtAction::Decode { token } => {
22            let parts = decode(&token)?;
23            println!("Header:    {}", parts.header);
24            println!("Payload:   {}", parts.payload);
25            println!("Signature: {}", parts.signature_hex);
26        }
27        JwtAction::Analyze { token } => {
28            let parts = decode(&token)?;
29            println!("Header:    {}", parts.header);
30            println!("Payload:   {}", parts.payload);
31            println!("Algorithm: {}", extract_algorithm(&parts.header));
32            println!();
33            let warnings = find_vulnerabilities(&parts.header);
34            if warnings.is_empty() {
35                println!("No obvious vulnerabilities detected.");
36            } else {
37                println!("Potential vulnerabilities:");
38                for w in &warnings {
39                    println!("  [!] {}", w);
40                }
41            }
42        }
43    }
44    Ok(())
45}
46
47pub struct JwtParts {
48    pub header: String,
49    pub payload: String,
50    pub signature_hex: String,
51}
52
53pub fn decode(token: &str) -> Result<JwtParts> {
54    let token = token.trim();
55    let segments: Vec<&str> = token.split('.').collect();
56    if segments.len() != 3 {
57        anyhow::bail!(
58            "Invalid JWT format: expected 3 dot-separated parts, got {}",
59            segments.len()
60        );
61    }
62
63    let header_bytes = URL_SAFE_NO_PAD
64        .decode(segments[0])
65        .context("Failed to decode JWT header (invalid base64url)")?;
66    let header = String::from_utf8(header_bytes).context("JWT header is not valid UTF-8")?;
67
68    let payload_bytes = URL_SAFE_NO_PAD
69        .decode(segments[1])
70        .context("Failed to decode JWT payload (invalid base64url)")?;
71    let payload = String::from_utf8(payload_bytes).context("JWT payload is not valid UTF-8")?;
72
73    let sig_bytes = URL_SAFE_NO_PAD
74        .decode(segments[2])
75        .context("Failed to decode JWT signature (invalid base64url)")?;
76    let signature_hex = hex::encode(&sig_bytes);
77
78    Ok(JwtParts {
79        header,
80        payload,
81        signature_hex,
82    })
83}
84
85pub fn extract_algorithm(header_json: &str) -> String {
86    let v: serde_json::Value = serde_json::from_str(header_json).unwrap_or(serde_json::Value::Null);
87    v.get("alg")
88        .and_then(|v| v.as_str())
89        .unwrap_or("unknown")
90        .to_string()
91}
92
93pub fn find_vulnerabilities(header_json: &str) -> Vec<String> {
94    let mut warnings = Vec::new();
95
96    // Parse JSON safely using serde_json
97    let v: serde_json::Value = match serde_json::from_str(header_json) {
98        Ok(val) => val,
99        Err(_) => return vec!["Invalid JSON header - parsing failed".to_string()],
100    };
101
102    let alg = v.get("alg").and_then(|v| v.as_str()).unwrap_or("unknown");
103    let alg_lower = alg.to_lowercase();
104
105    if alg_lower == "none" {
106        warnings.push("Algorithm is \"none\" - signature verification is disabled!".to_string());
107    }
108
109    if alg_lower == "hs256" || alg_lower == "hs384" || alg_lower == "hs512" {
110        warnings.push(format!(
111            "Symmetric algorithm ({}) - check for algorithm confusion attacks (RS256 -> HS256)",
112            alg
113        ));
114    }
115
116    if v.get("jku").is_some() {
117        warnings.push(
118            "\"jku\" (JWK Set URL) header present - possible SSRF or key injection".to_string(),
119        );
120    }
121
122    if v.get("x5u").is_some() {
123        warnings.push(
124            "\"x5u\" (X.509 URL) header present - possible SSRF or key injection".to_string(),
125        );
126    }
127
128    if v.get("kid").is_some() {
129        warnings.push(
130            "\"kid\" (Key ID) header present - check for SQL injection or path traversal"
131                .to_string(),
132        );
133    }
134
135    if v.get("jwk").is_some() {
136        warnings.push(
137            "\"jwk\" (embedded key) header present - possible key self-signing attack".to_string(),
138        );
139    }
140
141    warnings
142}