use crate::{Transform, TransformError, TransformerCategory};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JwtDecode;
impl Transform for JwtDecode {
fn name(&self) -> &'static str {
"JWT Decoder"
}
fn id(&self) -> &'static str {
"jwtdecode"
}
fn description(&self) -> &'static str {
"Decodes a JSON Web Token (JWT) without verifying the signature."
}
fn category(&self) -> TransformerCategory {
TransformerCategory::Decoder
}
fn default_test_input(&self) -> &'static str {
"eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
}
fn transform(&self, input: &str) -> Result<String, TransformError> {
let parts: Vec<&str> = input.trim().split('.').collect();
if parts.len() != 3 {
return Err(TransformError::InvalidArgument(
"JWT must have three parts separated by dots."
.to_string()
.into(),
));
}
let header_b64url = parts[0];
let payload_b64url = parts[1];
let header_bytes = base64url_decode(header_b64url)?;
let payload_bytes = base64url_decode(payload_b64url)?;
let header_json = String::from_utf8(header_bytes).map_err(|e| {
TransformError::InvalidArgument(format!("Header is not valid UTF-8: {}", e).into())
})?;
let payload_json = String::from_utf8(payload_bytes).map_err(|e| {
TransformError::InvalidArgument(format!("Payload is not valid UTF-8: {}", e).into())
})?;
let output = format!(
"Header:\n{}\n\nPayload:\n{}\n\n(Signature not verified)",
header_json, payload_json
);
Ok(output)
}
}
fn base64url_decode(input: &str) -> Result<Vec<u8>, TransformError> {
let mut base64_str = input.replace('-', "+").replace('_', "/");
match base64_str.len() % 4 {
2 => base64_str.push_str("=="),
3 => base64_str.push('='),
0 => (), _ => return Err(TransformError::Base64DecodeError), }
base64_standard_decode(&base64_str)
}
const LOOKUP_TABLE: [i8; 256] = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ];
fn base64_standard_decode(input: &str) -> Result<Vec<u8>, TransformError> {
let input_bytes = input.trim().as_bytes();
let padding = input_bytes.iter().rev().take_while(|&&c| c == b'=').count();
if input_bytes.iter().rev().skip(padding).any(|&c| c == b'=') {
return Err(TransformError::Base64DecodeError);
}
if padding > 2 {
return Err(TransformError::Base64DecodeError);
}
if !input_bytes.len().is_multiple_of(4) {
return Err(TransformError::Base64DecodeError);
}
let output_len = (input_bytes.len() / 4) * 3 - padding;
let mut output = vec![0u8; output_len];
let mut output_index = 0;
for chunk in input_bytes.chunks_exact(4) {
let b0 = LOOKUP_TABLE[chunk[0] as usize];
let b1 = LOOKUP_TABLE[chunk[1] as usize];
if b0 < 0 || b1 < 0 {
return Err(TransformError::Base64DecodeError);
}
if output_index < output_len {
output[output_index] = ((b0 << 2) | (b1 >> 4)) as u8;
output_index += 1;
}
if chunk[2] == b'=' {
if chunk[3] != b'=' {
return Err(TransformError::Base64DecodeError);
}
break;
}
let b2 = LOOKUP_TABLE[chunk[2] as usize];
if b2 < 0 {
return Err(TransformError::Base64DecodeError);
}
if output_index < output_len {
output[output_index] = (((b1 & 0xF) << 4) | (b2 >> 2)) as u8;
output_index += 1;
}
if chunk[3] == b'=' {
break;
}
let b3 = LOOKUP_TABLE[chunk[3] as usize];
if b3 < 0 {
return Err(TransformError::Base64DecodeError);
}
if output_index < output_len {
output[output_index] = (((b2 & 0x3) << 6) | b3) as u8;
output_index += 1;
}
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TransformError;
const EXAMPLE_JWT: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const EXPECTED_HEADER: &str = r#"{"alg":"HS256","typ":"JWT"}"#;
const EXPECTED_PAYLOAD: &str = r#"{"sub":"1234567890","name":"John Doe","iat":1516239022}"#;
#[test]
fn test_jwt_decode_valid() {
let transformer = JwtDecode;
let expected_header = "{\"alg\":\"none\"}";
let expected_payload = "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
let expected_output = format!(
"Header:\n{}\n\nPayload:\n{}\n\n(Signature not verified)",
expected_header, expected_payload
);
assert_eq!(
transformer
.transform(transformer.default_test_input())
.unwrap(),
expected_output
);
let expected_hs256_output = format!(
"Header:\n{}\n\nPayload:\n{}\n\n(Signature not verified)",
EXPECTED_HEADER, EXPECTED_PAYLOAD
);
assert_eq!(
transformer.transform(EXAMPLE_JWT).unwrap(),
expected_hs256_output
);
}
#[test]
fn test_jwt_decode_invalid_parts() {
let transformer = JwtDecode;
assert!(matches!(
transformer.transform("invalid"),
Err(TransformError::InvalidArgument(_))
));
assert!(matches!(
transformer.transform("a.b"),
Err(TransformError::InvalidArgument(_))
));
assert!(matches!(
transformer.transform("a.b.c.d"),
Err(TransformError::InvalidArgument(_))
));
}
#[test]
fn test_jwt_decode_invalid_base64() {
let transformer = JwtDecode;
let jwt_bad_header = "@@@.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig";
assert!(matches!(
transformer.transform(jwt_bad_header),
Err(TransformError::Base64DecodeError)
));
let jwt_bad_payload = "eyJhbGciOiJIUzI1NiJ9.@@@.sig";
assert!(matches!(
transformer.transform(jwt_bad_payload),
Err(TransformError::Base64DecodeError)
));
}
#[test]
fn test_jwt_decode_invalid_utf8() {
let transformer = JwtDecode;
let header_invalid_utf8 = "wyg";
let payload_valid = "eyJzdWIiOiIxMjM0NTY3ODkwIn0";
let jwt = format!("{}.{}.sig", header_invalid_utf8, payload_valid);
assert!(
matches!(transformer.transform(&jwt), Err(TransformError::InvalidArgument(msg)) if msg.contains("Header is not valid UTF-8"))
);
let header_valid = "eyJhbGciOiJIUzI1NiJ9";
let payload_invalid_utf8 = "wyg";
let jwt = format!("{}.{}.sig", header_valid, payload_invalid_utf8);
assert!(
matches!(transformer.transform(&jwt), Err(TransformError::InvalidArgument(msg)) if msg.contains("Payload is not valid UTF-8"))
);
}
#[test]
fn test_base64url_decode_internal() {
assert_eq!(
base64url_decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").unwrap(),
EXPECTED_HEADER.as_bytes()
);
assert_eq!(
base64url_decode(
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
)
.unwrap(),
EXPECTED_PAYLOAD.as_bytes()
);
assert_eq!(base64url_decode("YQ").unwrap(), b"a");
assert_eq!(base64url_decode("YWI").unwrap(), b"ab");
assert_eq!(base64url_decode("YWJj").unwrap(), b"abc");
assert_eq!(base64url_decode("_-8").unwrap(), b"\xff\xef");
}
#[test]
fn test_base64_standard_decode_errors() {
assert!(matches!(
base64_standard_decode("YQ==="),
Err(TransformError::Base64DecodeError)
));
assert!(matches!(
base64_standard_decode("YQ=a"),
Err(TransformError::Base64DecodeError)
));
assert!(matches!(
base64_standard_decode("YQ!="),
Err(TransformError::Base64DecodeError)
));
assert!(matches!(
base64_standard_decode("Y"),
Err(TransformError::Base64DecodeError)
));
}
}