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