1use anyhow::{anyhow, Result};
3use base64::Engine;
4use colored::Colorize;
5
6pub 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 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 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 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
62pub 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 if !decoded.encryption.contains("CBC") {
73 return Err(anyhow!(
74 "Padding oracle attacks only apply to CBC mode encryption"
75 ));
76 }
77
78 payloads.push(token.to_string());
80
81 let parts: Vec<&str> = token.split('.').collect();
83 let original_header = parts[0];
84
85 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 let reassemble = |header: &str, ek: &str, iv: &str, ct: &str, tag: &str| -> String {
92 format!("{header}.{ek}.{iv}.{ct}.{tag}")
93 };
94
95 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 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 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 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 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
171pub 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
183pub 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 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); 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 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 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 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 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 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 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}