use anyhow::{anyhow, Result};
use base64::Engine;
use colored::Colorize;
pub fn detect_padding_oracle_vulnerability(token: &str) -> Result<Vec<String>> {
use crate::jwt;
let decoded = jwt::decode_jwe(token)?;
let mut warnings = Vec::new();
match decoded.encryption.as_str() {
"A128CBC-HS256" | "A192CBC-HS384" | "A256CBC-HS512" => {
warnings.push(format!(
"⚠️ CRITICAL: {} uses CBC mode - potentially vulnerable to padding oracle attacks",
decoded.encryption.red().bold()
));
warnings.push(
" Attack: Send modified ciphertext and observe error responses".to_string(),
);
warnings.push(" If server returns different errors for padding vs MAC failures, it may be exploitable".to_string());
}
"A128GCM" | "A256GCM" => {
warnings.push(format!(
"✓ {} uses GCM mode - not vulnerable to classic padding oracle attacks",
decoded.encryption.green()
));
}
_ => {
warnings.push(format!(
"⚠️ Unknown encryption algorithm: {}",
decoded.encryption
));
}
}
if decoded.algorithm == "RSA-OAEP" || decoded.algorithm == "RSA-OAEP-256" {
warnings.push("⚠️ RSA key wrapping - may be vulnerable to timing attacks".to_string());
}
if !decoded.tag.is_empty() {
if let Ok(tag_bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&decoded.tag)
{
if tag_bytes.len() < 16 {
warnings.push(
"⚠️ Short authentication tag - may indicate weak implementation".to_string(),
);
}
}
}
Ok(warnings)
}
pub fn generate_padding_oracle_payloads(token: &str) -> Result<Vec<String>> {
use crate::jwt;
let decoded = jwt::decode_jwe(token)?;
let mut payloads = Vec::new();
if !decoded.encryption.contains("CBC") {
return Err(anyhow!(
"Padding oracle attacks only apply to CBC mode encryption"
));
}
payloads.push(token.to_string());
let parts: Vec<&str> = token.split('.').collect();
let original_header = parts[0];
let ciphertext_bytes =
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&decoded.ciphertext)?;
let iv_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&decoded.iv)?;
let reassemble = |header: &str, ek: &str, iv: &str, ct: &str, tag: &str| -> String {
format!("{header}.{ek}.{iv}.{ct}.{tag}")
};
if !ciphertext_bytes.is_empty() {
let mut modified = ciphertext_bytes.clone();
if let Some(last) = modified.last_mut() {
*last ^= 0x01;
}
let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
payloads.push(reassemble(
original_header,
&decoded.encrypted_key,
&decoded.iv,
&ct,
&decoded.tag,
));
}
if ciphertext_bytes.len() > 16 {
let mut modified = ciphertext_bytes.clone();
modified.truncate(modified.len() - 8);
let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
payloads.push(reassemble(
original_header,
&decoded.encrypted_key,
&decoded.iv,
&ct,
&decoded.tag,
));
}
if !ciphertext_bytes.is_empty() {
let mut modified = ciphertext_bytes.clone();
modified[0] ^= 0xFF;
let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
payloads.push(reassemble(
original_header,
&decoded.encrypted_key,
&decoded.iv,
&ct,
&decoded.tag,
));
}
if ciphertext_bytes.len() >= 16 {
let mut modified = ciphertext_bytes.clone();
let start = modified.len() - 16;
modified[start..].fill(0x00);
let ct = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
payloads.push(reassemble(
original_header,
&decoded.encrypted_key,
&decoded.iv,
&ct,
&decoded.tag,
));
}
if !iv_bytes.is_empty() {
let mut modified = iv_bytes.clone();
modified[0] ^= 0x01;
let iv = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&modified);
payloads.push(reassemble(
original_header,
&decoded.encrypted_key,
&iv,
&decoded.ciphertext,
&decoded.tag,
));
}
Ok(payloads)
}
pub fn analyze_response_for_oracle(
response_time_ms: u64,
status_code: u16,
error_message: Option<&str>,
) -> Vec<String> {
analyze_response_for_oracle_with_baseline(response_time_ms, status_code, error_message, None)
}
pub fn analyze_response_for_oracle_with_baseline(
response_time_ms: u64,
status_code: u16,
error_message: Option<&str>,
baseline_times_ms: Option<&[u64]>,
) -> Vec<String> {
let mut indicators = Vec::new();
match baseline_times_ms {
Some(baselines) if baselines.len() >= 2 => {
let n = baselines.len() as f64;
let mean = baselines.iter().sum::<u64>() as f64 / n;
let variance = baselines
.iter()
.map(|&t| {
let diff = t as f64 - mean;
diff * diff
})
.sum::<f64>()
/ n;
let stddev = variance.sqrt();
let threshold = mean + 2.0 * stddev.max(5.0);
if (response_time_ms as f64) > threshold {
indicators.push(format!(
"⚠️ Timing anomaly: {}ms (baseline: {:.1}ms ± {:.1}ms, threshold: {:.1}ms)",
response_time_ms, mean, stddev, threshold
));
}
}
_ => {
if response_time_ms > 100 {
indicators.push(format!(
"⚠️ Slow response ({}ms) - may indicate server-side processing differences",
response_time_ms
));
}
}
}
if let Some(msg) = error_message {
let msg_lower = msg.to_lowercase();
if msg_lower.contains("padding") {
indicators.push(
"🚨 PADDING ERROR DETECTED - Strong oracle indicator!"
.red()
.bold()
.to_string(),
);
} else if msg_lower.contains("mac") || msg_lower.contains("authentication") {
indicators
.push("⚠️ MAC/Authentication error - Different from padding error".to_string());
} else if msg_lower.contains("decrypt") {
indicators.push("⚠️ Generic decryption error - May hide oracle".to_string());
}
}
match status_code {
400 => indicators.push("Status 400 - Bad Request (common for padding errors)".to_string()),
401 => indicators.push("Status 401 - Unauthorized (may indicate MAC failure)".to_string()),
500 => indicators.push("⚠️ Status 500 - Server error may leak information".to_string()),
_ => {}
}
if indicators.is_empty() {
indicators.push("No obvious oracle indicators detected".to_string());
}
indicators
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_padding_oracle_vulnerability() {
let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
let result = detect_padding_oracle_vulnerability(jwe_token);
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(!warnings.is_empty(), "Should detect CBC mode vulnerability");
}
#[test]
fn test_generate_padding_oracle_payloads() {
let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
let result = generate_padding_oracle_payloads(jwe_token);
assert!(result.is_ok());
let payloads = result.unwrap();
assert!(payloads.len() > 1, "Should generate multiple payloads");
}
#[test]
fn test_analyze_response_for_oracle() {
let indicators = analyze_response_for_oracle(50, 400, Some("Invalid padding"));
assert!(!indicators.is_empty());
assert!(indicators.iter().any(|i| i.contains("PADDING")));
}
#[test]
fn test_analyze_response_timing() {
let indicators = analyze_response_for_oracle(150, 200, None);
assert!(indicators.iter().any(|i| i.contains("Slow response")));
}
#[test]
fn test_detect_gcm_mode_not_vulnerable() {
let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
let result = detect_padding_oracle_vulnerability(jwe_token);
assert!(result.is_ok());
let warnings = result.unwrap();
assert!(
warnings
.iter()
.any(|w| w.contains("GCM") && w.contains("not vulnerable")),
"GCM mode should be reported as not vulnerable"
);
}
#[test]
fn test_generate_padding_oracle_payloads_gcm_rejected() {
let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
let result = generate_padding_oracle_payloads(jwe_token);
assert!(
result.is_err(),
"GCM mode should not generate padding oracle payloads"
);
}
#[test]
fn test_generate_padding_oracle_payloads_count() {
let jwe_token = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..ZHVtbXlfaXZfMTIzNDU2.eyJzdWIiOiJ0ZXN0In0.ZHVtbXlfdGFn";
let result = generate_padding_oracle_payloads(jwe_token);
assert!(result.is_ok());
let payloads = result.unwrap();
assert!(
payloads.len() >= 4,
"Should generate at least 4 payloads (original + 3 modifications), got {}",
payloads.len()
);
}
#[test]
fn test_analyze_response_mac_error() {
let indicators = analyze_response_for_oracle(50, 401, Some("MAC verification failed"));
assert!(indicators.iter().any(|i| i.contains("MAC")));
}
#[test]
fn test_analyze_response_decrypt_error() {
let indicators = analyze_response_for_oracle(50, 500, Some("Decryption failed"));
assert!(indicators.iter().any(|i| i.contains("decryption")));
assert!(indicators.iter().any(|i| i.contains("500")));
}
#[test]
fn test_analyze_response_no_indicators() {
let indicators = analyze_response_for_oracle(50, 200, None);
assert!(indicators.iter().any(|i| i.contains("No obvious oracle")));
}
#[test]
fn test_analyze_response_with_baseline_normal() {
let baselines = vec![50, 52, 48, 51, 49];
let indicators = analyze_response_for_oracle_with_baseline(55, 200, None, Some(&baselines));
assert!(
!indicators.iter().any(|i| i.contains("Timing anomaly")),
"Normal response should not trigger timing anomaly"
);
}
#[test]
fn test_analyze_response_with_baseline_anomaly() {
let baselines = vec![50, 52, 48, 51, 49];
let indicators =
analyze_response_for_oracle_with_baseline(200, 200, None, Some(&baselines));
assert!(
indicators.iter().any(|i| i.contains("Timing anomaly")),
"200ms response against ~50ms baseline should trigger anomaly"
);
}
}