1use crate::{Transform, TransformError, TransformerCategory};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct JwtDecode;
6
7impl Transform for JwtDecode {
8 fn name(&self) -> &'static str {
9 "JWT Decoder"
10 }
11
12 fn id(&self) -> &'static str {
13 "jwtdecode"
14 }
15
16 fn description(&self) -> &'static str {
17 "Decodes a JSON Web Token (JWT) without verifying the signature."
18 }
19
20 fn category(&self) -> TransformerCategory {
21 TransformerCategory::Decoder
22 }
23
24 fn default_test_input(&self) -> &'static str {
25 "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
26 }
27
28 fn transform(&self, input: &str) -> Result<String, TransformError> {
29 let parts: Vec<&str> = input.trim().split('.').collect();
30
31 if parts.len() != 3 {
32 return Err(TransformError::InvalidArgument(
33 "JWT must have three parts separated by dots."
34 .to_string()
35 .into(),
36 ));
37 }
38
39 let header_b64url = parts[0];
40 let payload_b64url = parts[1];
41
42 let header_bytes = base64url_decode(header_b64url)?;
43 let payload_bytes = base64url_decode(payload_b64url)?;
44
45 let header_json = String::from_utf8(header_bytes).map_err(|e| {
46 TransformError::InvalidArgument(format!("Header is not valid UTF-8: {}", e).into())
47 })?;
48 let payload_json = String::from_utf8(payload_bytes).map_err(|e| {
49 TransformError::InvalidArgument(format!("Payload is not valid UTF-8: {}", e).into())
50 })?;
51
52 let output = format!(
53 "Header:\n{}\n\nPayload:\n{}\n\n(Signature not verified)",
54 header_json, payload_json
55 );
56
57 Ok(output)
58 }
59}
60
61fn base64url_decode(input: &str) -> Result<Vec<u8>, TransformError> {
62 let mut base64_str = input.replace('-', "+").replace('_', "/");
63 match base64_str.len() % 4 {
64 2 => base64_str.push_str("=="),
65 3 => base64_str.push('='),
66 0 => (), _ => return Err(TransformError::Base64DecodeError), }
69 base64_standard_decode(&base64_str)
70}
71
72const LOOKUP_TABLE: [i8; 256] = [
73 -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, ];
91
92fn base64_standard_decode(input: &str) -> Result<Vec<u8>, TransformError> {
93 let input_bytes = input.trim().as_bytes();
94 let padding = input_bytes.iter().rev().take_while(|&&c| c == b'=').count();
95
96 if input_bytes.iter().rev().skip(padding).any(|&c| c == b'=') {
97 return Err(TransformError::Base64DecodeError);
98 }
99 if padding > 2 {
100 return Err(TransformError::Base64DecodeError);
101 }
102 if !input_bytes.len().is_multiple_of(4) {
103 return Err(TransformError::Base64DecodeError);
104 }
105
106 let output_len = (input_bytes.len() / 4) * 3 - padding;
107 let mut output = vec![0u8; output_len];
108 let mut output_index = 0;
109
110 for chunk in input_bytes.chunks_exact(4) {
111 let b0 = LOOKUP_TABLE[chunk[0] as usize];
112 let b1 = LOOKUP_TABLE[chunk[1] as usize];
113
114 if b0 < 0 || b1 < 0 {
115 return Err(TransformError::Base64DecodeError);
116 }
117
118 if output_index < output_len {
119 output[output_index] = ((b0 << 2) | (b1 >> 4)) as u8;
120 output_index += 1;
121 }
122
123 if chunk[2] == b'=' {
124 if chunk[3] != b'=' {
125 return Err(TransformError::Base64DecodeError);
126 }
127 break;
128 }
129
130 let b2 = LOOKUP_TABLE[chunk[2] as usize];
131 if b2 < 0 {
132 return Err(TransformError::Base64DecodeError);
133 }
134
135 if output_index < output_len {
136 output[output_index] = (((b1 & 0xF) << 4) | (b2 >> 2)) as u8;
137 output_index += 1;
138 }
139
140 if chunk[3] == b'=' {
141 break;
142 }
143
144 let b3 = LOOKUP_TABLE[chunk[3] as usize];
145 if b3 < 0 {
146 return Err(TransformError::Base64DecodeError);
147 }
148
149 if output_index < output_len {
150 output[output_index] = (((b2 & 0x3) << 6) | b3) as u8;
151 output_index += 1;
152 }
153 }
154
155 Ok(output)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::TransformError;
162
163 const EXAMPLE_JWT: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
164 const EXPECTED_HEADER: &str = r#"{"alg":"HS256","typ":"JWT"}"#;
165 const EXPECTED_PAYLOAD: &str = r#"{"sub":"1234567890","name":"John Doe","iat":1516239022}"#;
166
167 #[test]
168 fn test_jwt_decode_valid() {
169 let transformer = JwtDecode;
170 let expected_header = "{\"alg\":\"none\"}";
171 let expected_payload = "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
172 let expected_output = format!(
173 "Header:\n{}\n\nPayload:\n{}\n\n(Signature not verified)",
174 expected_header, expected_payload
175 );
176 assert_eq!(
177 transformer
178 .transform(transformer.default_test_input())
179 .unwrap(),
180 expected_output
181 );
182
183 let expected_hs256_output = format!(
185 "Header:\n{}\n\nPayload:\n{}\n\n(Signature not verified)",
186 EXPECTED_HEADER, EXPECTED_PAYLOAD
187 );
188 assert_eq!(
189 transformer.transform(EXAMPLE_JWT).unwrap(),
190 expected_hs256_output
191 );
192 }
193
194 #[test]
195 fn test_jwt_decode_invalid_parts() {
196 let transformer = JwtDecode;
197 assert!(matches!(
198 transformer.transform("invalid"),
199 Err(TransformError::InvalidArgument(_))
200 ));
201 assert!(matches!(
202 transformer.transform("a.b"),
203 Err(TransformError::InvalidArgument(_))
204 ));
205 assert!(matches!(
206 transformer.transform("a.b.c.d"),
207 Err(TransformError::InvalidArgument(_))
208 ));
209 }
210
211 #[test]
212 fn test_jwt_decode_invalid_base64() {
213 let transformer = JwtDecode;
214 let jwt_bad_header = "@@@.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig";
215 assert!(matches!(
216 transformer.transform(jwt_bad_header),
217 Err(TransformError::Base64DecodeError)
218 ));
219 let jwt_bad_payload = "eyJhbGciOiJIUzI1NiJ9.@@@.sig";
220 assert!(matches!(
221 transformer.transform(jwt_bad_payload),
222 Err(TransformError::Base64DecodeError)
223 ));
224 }
225
226 #[test]
227 fn test_jwt_decode_invalid_utf8() {
228 let transformer = JwtDecode;
229 let header_invalid_utf8 = "wyg";
230 let payload_valid = "eyJzdWIiOiIxMjM0NTY3ODkwIn0";
231 let jwt = format!("{}.{}.sig", header_invalid_utf8, payload_valid);
232 assert!(
233 matches!(transformer.transform(&jwt), Err(TransformError::InvalidArgument(msg)) if msg.contains("Header is not valid UTF-8"))
234 );
235 let header_valid = "eyJhbGciOiJIUzI1NiJ9";
236 let payload_invalid_utf8 = "wyg";
237 let jwt = format!("{}.{}.sig", header_valid, payload_invalid_utf8);
238 assert!(
239 matches!(transformer.transform(&jwt), Err(TransformError::InvalidArgument(msg)) if msg.contains("Payload is not valid UTF-8"))
240 );
241 }
242
243 #[test]
244 fn test_base64url_decode_internal() {
245 assert_eq!(
246 base64url_decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").unwrap(),
247 EXPECTED_HEADER.as_bytes()
248 );
249 assert_eq!(
250 base64url_decode(
251 "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
252 )
253 .unwrap(),
254 EXPECTED_PAYLOAD.as_bytes()
255 );
256 assert_eq!(base64url_decode("YQ").unwrap(), b"a");
257 assert_eq!(base64url_decode("YWI").unwrap(), b"ab");
258 assert_eq!(base64url_decode("YWJj").unwrap(), b"abc");
259 assert_eq!(base64url_decode("_-8").unwrap(), b"\xff\xef");
260 }
261
262 #[test]
263 fn test_base64_standard_decode_errors() {
264 assert!(matches!(
265 base64_standard_decode("YQ==="),
266 Err(TransformError::Base64DecodeError)
267 ));
268 assert!(matches!(
269 base64_standard_decode("YQ=a"),
270 Err(TransformError::Base64DecodeError)
271 ));
272 assert!(matches!(
273 base64_standard_decode("YQ!="),
274 Err(TransformError::Base64DecodeError)
275 ));
276 assert!(matches!(
277 base64_standard_decode("Y"),
278 Err(TransformError::Base64DecodeError)
279 ));
280 }
282}