Skip to main content

jwt_hack/
jwe_attacks.rs

1/// Module for JWE-specific attack capabilities
2use anyhow::{anyhow, Result};
3use base64::Engine;
4use colored::Colorize;
5
6/// Detect potential padding oracle vulnerabilities in JWE implementations
7///
8/// This function analyzes a JWE token to detect characteristics that may indicate
9/// susceptibility to padding oracle attacks, particularly with CBC mode encryption.
10pub fn detect_padding_oracle_vulnerability(token: &str) -> Result<Vec<String>> {
11    use crate::jwt;
12
13    let decoded = jwt::decode_jwe(token)?;
14    let mut warnings = Vec::new();
15
16    // Check if using CBC mode (vulnerable to padding oracle attacks)
17    match decoded.encryption.as_str() {
18        "A128CBC-HS256" | "A192CBC-HS384" | "A256CBC-HS512" => {
19            warnings.push(format!(
20                "⚠️  CRITICAL: {} uses CBC mode - potentially vulnerable to padding oracle attacks",
21                decoded.encryption.red().bold()
22            ));
23            warnings.push(
24                "   Attack: Send modified ciphertext and observe error responses".to_string(),
25            );
26            warnings.push("   If server returns different errors for padding vs MAC failures, it may be exploitable".to_string());
27        }
28        "A128GCM" | "A256GCM" => {
29            warnings.push(format!(
30                "✓ {} uses GCM mode - not vulnerable to classic padding oracle attacks",
31                decoded.encryption.green()
32            ));
33        }
34        _ => {
35            warnings.push(format!(
36                "⚠️  Unknown encryption algorithm: {}",
37                decoded.encryption
38            ));
39        }
40    }
41
42    // Check for potential timing attack vectors
43    if decoded.algorithm == "RSA-OAEP" || decoded.algorithm == "RSA-OAEP-256" {
44        warnings.push("⚠️  RSA key wrapping - may be vulnerable to timing attacks".to_string());
45    }
46
47    // Check authentication tag length
48    if !decoded.tag.is_empty() {
49        if let Ok(tag_bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&decoded.tag)
50        {
51            if tag_bytes.len() < 16 {
52                warnings.push(
53                    "⚠️  Short authentication tag - may indicate weak implementation".to_string(),
54                );
55            }
56        }
57    }
58
59    Ok(warnings)
60}
61
62/// Generate test payloads for padding oracle attacks
63///
64/// Creates modified JWE tokens to test server responses for padding oracle vulnerabilities
65pub fn generate_padding_oracle_payloads(token: &str) -> Result<Vec<String>> {
66    use crate::jwt;
67
68    let decoded = jwt::decode_jwe(token)?;
69    let mut payloads = Vec::new();
70
71    // Only generate payloads for CBC mode
72    if !decoded.encryption.contains("CBC") {
73        return Err(anyhow!(
74            "Padding oracle attacks only apply to CBC mode encryption"
75        ));
76    }
77
78    // Original token for baseline
79    payloads.push(token.to_string());
80
81    // Preserve the original header part to avoid re-serialization issues
82    let parts: Vec<&str> = token.split('.').collect();
83    let original_header = parts[0];
84
85    // Decode ciphertext and IV once for all mutations
86    let ciphertext_bytes =
87        base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&decoded.ciphertext)?;
88    let iv_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&decoded.iv)?;
89
90    // Helper to reassemble a JWE token from parts
91    let reassemble = |header: &str, ek: &str, iv: &str, ct: &str, tag: &str| -> String {
92        format!("{header}.{ek}.{iv}.{ct}.{tag}")
93    };
94
95    // Modify last byte of ciphertext (affects padding)
96    if !ciphertext_bytes.is_empty() {
97        let mut modified = ciphertext_bytes.clone();
98        if let Some(last) = modified.last_mut() {
99            *last ^= 0x01;
100        }
101        let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
102        payloads.push(reassemble(
103            original_header,
104            &decoded.encrypted_key,
105            &decoded.iv,
106            &ct,
107            &decoded.tag,
108        ));
109    }
110
111    // Truncate ciphertext to test padding validation
112    if ciphertext_bytes.len() > 16 {
113        let mut modified = ciphertext_bytes.clone();
114        modified.truncate(modified.len() - 8);
115        let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
116        payloads.push(reassemble(
117            original_header,
118            &decoded.encrypted_key,
119            &decoded.iv,
120            &ct,
121            &decoded.tag,
122        ));
123    }
124
125    // Flip first byte of ciphertext (affects first block)
126    if !ciphertext_bytes.is_empty() {
127        let mut modified = ciphertext_bytes.clone();
128        modified[0] ^= 0xFF;
129        let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
130        payloads.push(reassemble(
131            original_header,
132            &decoded.encrypted_key,
133            &decoded.iv,
134            &ct,
135            &decoded.tag,
136        ));
137    }
138
139    // Replace entire last block with zeros (16-byte block boundary)
140    if ciphertext_bytes.len() >= 16 {
141        let mut modified = ciphertext_bytes.clone();
142        let start = modified.len() - 16;
143        modified[start..].fill(0x00);
144        let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
145        payloads.push(reassemble(
146            original_header,
147            &decoded.encrypted_key,
148            &decoded.iv,
149            &ct,
150            &decoded.tag,
151        ));
152    }
153
154    // Modify IV to test CBC chaining (affects first plaintext block)
155    if !iv_bytes.is_empty() {
156        let mut modified = iv_bytes.clone();
157        modified[0] ^= 0x01;
158        let iv = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
159        payloads.push(reassemble(
160            original_header,
161            &decoded.encrypted_key,
162            &iv,
163            &decoded.ciphertext,
164            &decoded.tag,
165        ));
166    }
167
168    Ok(payloads)
169}
170
171/// Analyze server response patterns for padding oracle indicators
172///
173/// This function helps identify if a server may be vulnerable to padding oracle attacks
174/// by analyzing response characteristics
175pub fn analyze_response_for_oracle(
176    response_time_ms: u64,
177    status_code: u16,
178    error_message: Option<&str>,
179) -> Vec<String> {
180    analyze_response_for_oracle_with_baseline(response_time_ms, status_code, error_message, None)
181}
182
183/// Analyze server response patterns with optional baseline timing data.
184///
185/// When `baseline_times_ms` is provided, uses statistical comparison (mean + 2*stddev)
186/// to detect timing anomalies. Otherwise falls back to a fixed 100ms threshold.
187pub fn analyze_response_for_oracle_with_baseline(
188    response_time_ms: u64,
189    status_code: u16,
190    error_message: Option<&str>,
191    baseline_times_ms: Option<&[u64]>,
192) -> Vec<String> {
193    let mut indicators = Vec::new();
194
195    // Check for timing differences (potential timing side-channel)
196    match baseline_times_ms {
197        Some(baselines) if baselines.len() >= 2 => {
198            let n = baselines.len() as f64;
199            let mean = baselines.iter().sum::<u64>() as f64 / n;
200            let variance = baselines
201                .iter()
202                .map(|&t| {
203                    let diff = t as f64 - mean;
204                    diff * diff
205                })
206                .sum::<f64>()
207                / n;
208            let stddev = variance.sqrt();
209            let threshold = mean + 2.0 * stddev.max(5.0); // min stddev of 5ms to avoid false positives
210
211            if (response_time_ms as f64) > threshold {
212                indicators.push(format!(
213                    "⚠️  Timing anomaly: {}ms (baseline: {:.1}ms ± {:.1}ms, threshold: {:.1}ms)",
214                    response_time_ms, mean, stddev, threshold
215                ));
216            }
217        }
218        _ => {
219            if response_time_ms > 100 {
220                indicators.push(format!(
221                    "⚠️  Slow response ({}ms) - may indicate server-side processing differences",
222                    response_time_ms
223                ));
224            }
225        }
226    }
227
228    // Check for detailed error messages that leak information
229    if let Some(msg) = error_message {
230        let msg_lower = msg.to_lowercase();
231        if msg_lower.contains("padding") {
232            indicators.push(
233                "🚨 PADDING ERROR DETECTED - Strong oracle indicator!"
234                    .red()
235                    .bold()
236                    .to_string(),
237            );
238        } else if msg_lower.contains("mac") || msg_lower.contains("authentication") {
239            indicators
240                .push("⚠️  MAC/Authentication error - Different from padding error".to_string());
241        } else if msg_lower.contains("decrypt") {
242            indicators.push("⚠️  Generic decryption error - May hide oracle".to_string());
243        }
244    }
245
246    // Analyze status codes
247    match status_code {
248        400 => indicators.push("Status 400 - Bad Request (common for padding errors)".to_string()),
249        401 => indicators.push("Status 401 - Unauthorized (may indicate MAC failure)".to_string()),
250        500 => indicators.push("⚠️  Status 500 - Server error may leak information".to_string()),
251        _ => {}
252    }
253
254    if indicators.is_empty() {
255        indicators.push("No obvious oracle indicators detected".to_string());
256    }
257
258    indicators
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_detect_padding_oracle_vulnerability() {
267        let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
268        let result = detect_padding_oracle_vulnerability(jwe_token);
269        assert!(result.is_ok());
270        let warnings = result.unwrap();
271        assert!(!warnings.is_empty(), "Should detect CBC mode vulnerability");
272    }
273
274    #[test]
275    fn test_generate_padding_oracle_payloads() {
276        let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
277        let result = generate_padding_oracle_payloads(jwe_token);
278        assert!(result.is_ok());
279        let payloads = result.unwrap();
280        assert!(payloads.len() > 1, "Should generate multiple payloads");
281    }
282
283    #[test]
284    fn test_analyze_response_for_oracle() {
285        let indicators = analyze_response_for_oracle(50, 400, Some("Invalid padding"));
286        assert!(!indicators.is_empty());
287        assert!(indicators.iter().any(|i| i.contains("PADDING")));
288    }
289
290    #[test]
291    fn test_analyze_response_timing() {
292        let indicators = analyze_response_for_oracle(150, 200, None);
293        assert!(indicators.iter().any(|i| i.contains("Slow response")));
294    }
295
296    #[test]
297    fn test_detect_gcm_mode_not_vulnerable() {
298        // GCM mode header: {"alg":"dir","enc":"A256GCM"}
299        let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
300        let result = detect_padding_oracle_vulnerability(jwe_token);
301        assert!(result.is_ok());
302        let warnings = result.unwrap();
303        assert!(
304            warnings
305                .iter()
306                .any(|w| w.contains("GCM") && w.contains("not vulnerable")),
307            "GCM mode should be reported as not vulnerable"
308        );
309    }
310
311    #[test]
312    fn test_generate_padding_oracle_payloads_gcm_rejected() {
313        // GCM mode should be rejected for padding oracle payloads
314        let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
315        let result = generate_padding_oracle_payloads(jwe_token);
316        assert!(
317            result.is_err(),
318            "GCM mode should not generate padding oracle payloads"
319        );
320    }
321
322    #[test]
323    fn test_generate_padding_oracle_payloads_count() {
324        // CBC token should generate: original + bit flip + truncate + first byte flip + zero block + IV modify = 6
325        let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
326        let result = generate_padding_oracle_payloads(jwe_token);
327        assert!(result.is_ok());
328        let payloads = result.unwrap();
329        assert!(
330            payloads.len() >= 4,
331            "Should generate at least 4 payloads (original + 3 modifications), got {}",
332            payloads.len()
333        );
334    }
335
336    #[test]
337    fn test_analyze_response_mac_error() {
338        let indicators = analyze_response_for_oracle(50, 401, Some("MAC verification failed"));
339        assert!(indicators.iter().any(|i| i.contains("MAC")));
340    }
341
342    #[test]
343    fn test_analyze_response_decrypt_error() {
344        let indicators = analyze_response_for_oracle(50, 500, Some("Decryption failed"));
345        assert!(indicators.iter().any(|i| i.contains("decryption")));
346        assert!(indicators.iter().any(|i| i.contains("500")));
347    }
348
349    #[test]
350    fn test_analyze_response_no_indicators() {
351        let indicators = analyze_response_for_oracle(50, 200, None);
352        assert!(indicators.iter().any(|i| i.contains("No obvious oracle")));
353    }
354
355    #[test]
356    fn test_analyze_response_with_baseline_normal() {
357        let baselines = vec![50, 52, 48, 51, 49];
358        let indicators = analyze_response_for_oracle_with_baseline(55, 200, None, Some(&baselines));
359        // 55ms is within normal range of ~50ms baseline, should not trigger
360        assert!(
361            !indicators.iter().any(|i| i.contains("Timing anomaly")),
362            "Normal response should not trigger timing anomaly"
363        );
364    }
365
366    #[test]
367    fn test_analyze_response_with_baseline_anomaly() {
368        let baselines = vec![50, 52, 48, 51, 49];
369        let indicators =
370            analyze_response_for_oracle_with_baseline(200, 200, None, Some(&baselines));
371        assert!(
372            indicators.iter().any(|i| i.contains("Timing anomaly")),
373            "200ms response against ~50ms baseline should trigger anomaly"
374        );
375    }
376}