From cbba5c0963cfab4f9432e4a3574baaf23515be65 Mon Sep 17 00:00:00 2001 From: Deepu Date: Tue, 16 Jan 2024 14:03:12 +0100 Subject: [PATCH] add more tests --- src/app/jwt_decoder.rs | 39 +++- src/app/jwt_encoder.rs | 254 ++++++++++++++++++++------- test_data/test_ecdsa_private_key.pk8 | Bin 0 -> 138 bytes test_data/test_ecdsa_public_key.pk8 | 1 + 4 files changed, 223 insertions(+), 71 deletions(-) create mode 100644 test_data/test_ecdsa_private_key.pk8 create mode 100644 test_data/test_ecdsa_public_key.pk8 diff --git a/src/app/jwt_decoder.rs b/src/app/jwt_decoder.rs index 667385b..ece8460 100644 --- a/src/app/jwt_decoder.rs +++ b/src/app/jwt_decoder.rs @@ -123,12 +123,12 @@ impl TokenOutput { } #[derive(Debug, Clone)] -struct DecodeArgs { +pub(super) struct DecodeArgs { /// The JWT to decode. pub jwt: String, /// Display unix timestamps as ISO 8601 UTC dates pub time_format_utc: bool, - /// The secret to validate the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes + /// The secret to validate the JWT with. pub secret: String, /// Ignore token expiration date (`exp` claim) during validation pub ignore_exp: bool, @@ -185,7 +185,7 @@ pub fn print_decoded_token(token: &TokenData, json: bool) { } /// returns the base64 decoded values and signature verified result -fn decode_token( +pub(super) fn decode_token( arguments: &DecodeArgs, ) -> (JWTResult>, JWTResult>) { let header = match decode_header(&arguments.jwt) { @@ -309,7 +309,7 @@ mod tests { use super::*; #[test] - fn test_decode_hs256_token_with_valid_jwt_and_secret() { + fn test_decode_hmac_token_with_valid_jwt_and_secret() { let args = DecodeArgs { jwt: String::from("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"), secret: String::from("your-256-bit-secret"), @@ -334,7 +334,7 @@ mod tests { } #[test] - fn test_decode_hs256_token_with_valid_jwt_and_b64secret() { + fn test_decode_hmac_token_with_valid_jwt_and_b64secret() { let args = DecodeArgs { jwt: String::from("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.DCwemWTIxJURgfU0rFIIo20__ZAhQbl3ZpQ44nf6Mqs"), secret: String::from("b64:eW91ci0yNTYtYml0LXNlY3JldAo="), @@ -359,7 +359,7 @@ mod tests { } #[test] - fn test_decode_rs256_token_with_valid_jwt() { + fn test_decode_rsa_token_with_valid_jwt_and_invalid_signature() { let args = DecodeArgs { jwt: String::from("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkRGbzcxemxOdV9vLTkxOFJIN0lIVyJ9.eyJodHRwczovL3d3dy5qaGlwc3Rlci50ZWNoL3JvbGVzIjpbIkFkbWluaXN0cmF0b3IiLCJST0xFX0FETUlOIiwiUk9MRV9VU0VSIl0sImlzcyI6Imh0dHBzOi8vZGV2LTA2YnpzMWN1LnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2MWJjYmM3NmY2NGQ0YTAwNzJhZjhhMWQiLCJhdWQiOlsiaHR0cHM6Ly9kZXYtMDZienMxY3UudXMuYXV0aDAuY29tL2FwaS92Mi8iLCJodHRwczovL2Rldi0wNmJ6czFjdS51cy5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNzA1MDAyMDQxLCJleHAiOjE3MDUwODg0NDEsImF6cCI6IjFmbTdJMUdHRXRNZlRabW5vdFV1azVVT3gyWm10NnR0Iiwic2NvcGUiOiJvcGVuaWQifQ.eWdbVEolnmqqyx_Z5rR-09H3kg06EaokYoAAdrqLmB6FHwZbbyZrPaHImmEnY8BSRM42FpE9NZehqVAeQ5VQhOVdMMklCQSA5h13oQbKn6ciuc9Etyq2jg4sk2lOEkSmw4e_hWUGjkXnzP_J84o9-2qpN7VKNTGEvtk3mdQYXxwoeD8RvQjYJq6LsKIKA0biEyGWZxIpK1LCAFH1dmo5ZMpTeNGIwnUBdOxkL4jbKe26e9t7TDO0EtFjXmq-C218bbr1AgFN2eyj6n-3kNy9XfRcnfIlyXWJ0ZvcDVa9UoaTGP9Wdo0Ze3q2IrcgYrP7zTeZia5O2tejkaNknKNnwA"), secret: "".into(), @@ -385,7 +385,30 @@ mod tests { } #[test] - fn test_decode_es384_token_with_valid_jwt_and_secret_pem() { + fn test_decode_rsa_pss_token_with_valid_jwt_and_secret() { + let args = DecodeArgs { + jwt: String::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.a6yeSQkIfGD1Va9TgdImZUZ1AKO0OgP15ZFV4JPpZy8TpeByQpqUA3r2kJHNeUlETyEeYMKsDbZI5dYOEa_ZfF9xY6eslV1xmawOPkJYzf8IK3Lb42GEykn9qBWSvHzh5xFs2U1dYjJ9GW7bqhyPVaRVRKh1EBw8AbXmEYT42xSDnzkVUHhPpGM8_2anJNXvnexCQKlVRVVzZC04eHNsRIl5_n50irg7bQCO4z24kwViMTuCQTalV9LXCfdxp7_3Pp4Av_iJtkKHDXWs9GrrD6ttq1J6jOXDSbxn42XrPlxirr0pNtdvbk58W2LqYz4_G9q0HTRz_WO3FmaSxIxyqQ"), + secret: "@./test_data/test_rsa_public_key.pem".into(), + time_format_utc: false, + ignore_exp: true, + }; + + let (decode_only, verified_token_data) = decode_token(&args); + + assert!(decode_only.is_ok()); + assert!(verified_token_data.is_ok()); + + let decode_only_token = decode_only.unwrap(); + + assert_eq!(decode_only_token.header.alg, Algorithm::RS256); + assert_eq!( + format!("{:?}", decode_only_token.claims.0.get("name").unwrap()), + "String(\"John Doe\")" + ); + } + + #[test] + fn test_decode_ecdsa_token_with_valid_jwt_and_secret_pem() { let secret_file_name = "./test_data/test_ecdsa_public_key.pem"; let args = DecodeArgs { @@ -412,7 +435,7 @@ mod tests { } #[test] - fn test_decode_rs256_token_with_valid_jwt_and_secret_der() { + fn test_decode_rsa_token_with_valid_jwt_and_secret_der() { let secret_file_name = "./test_data/test_rsa_public_key.der"; let args = DecodeArgs { diff --git a/src/app/jwt_encoder.rs b/src/app/jwt_encoder.rs index b4e9c8c..2baeb7c 100644 --- a/src/app/jwt_encoder.rs +++ b/src/app/jwt_encoder.rs @@ -52,61 +52,59 @@ impl Encoder<'_> { } } +#[derive(Debug)] +struct EncodeArgs { + pub header: String, + /// claims + pub payload: String, + /// The secret to sign the JWT with. + pub secret: String, +} + pub fn encode_jwt_token(app: &mut App) { - let header = app.data.encoder.header.input.lines().join("\n"); - if header.is_empty() { - app.handle_error(String::from("Header should not be empty").into()); - return; + let out = encode_token(&EncodeArgs { + header: app.data.encoder.header.input.lines().join("\n"), + payload: app.data.encoder.payload.input.lines().join("\n"), + secret: app.data.encoder.secret.input.value().to_string(), + }); + + match out { + Ok(token) => { + if token != app.data.encoder.encoded.get_txt() { + app.data.encoder.encoded = ScrollableTxt::new(token); + app.data.encoder.signature_verified = true; + } + app.data.error = String::new(); + } + Err(e) => { + app.handle_error(e); + } + } +} + +fn encode_token(args: &EncodeArgs) -> JWTResult { + if args.header.is_empty() { + return Err(String::from("Header should not be empty").into()); } - let payload = app.data.encoder.payload.input.lines().join("\n"); - if payload.is_empty() { - app.handle_error(String::from("Payload should not be empty").into()); - return; + if args.payload.is_empty() { + return Err(String::from("Payload should not be empty").into()); } - let header: Result = serde_json::from_str(&header); + let header: Result = serde_json::from_str(&args.header); match header { Ok(header) => { let alg = header.alg; - let payload: Result = serde_json::from_str(&payload); + let payload: Result = serde_json::from_str(&args.payload); match payload { Ok(payload) => { - let secret = app.data.encoder.secret.input.value(); - let encoding_key = encoding_key_from_secret(&alg, secret); - match encoding_key { - Ok(encoding_key) => { - let token = jsonwebtoken::encode(&header, &payload, &encoding_key); - match token { - Ok(token) => { - if token != app.data.encoder.encoded.get_txt() { - app.data.encoder.encoded = ScrollableTxt::new(token); - app.data.encoder.signature_verified = true; - } - } - Err(e) => { - app.handle_error(e.into()); - return; - } - } - } - Err(e) => { - app.handle_error(e); - return; - } - } - } - Err(e) => { - app.handle_error(format!("Error parsing payload: {:}", e).into()); - return; + let encoding_key = encoding_key_from_secret(&alg, &args.secret)?; + Ok(jsonwebtoken::encode(&header, &payload, &encoding_key)?) } + Err(e) => Err(format!("Error parsing payload: {:}", e).into()), } } - Err(e) => { - app.handle_error(format!("Error parsing header: {:}", e).into()); - return; - } + Err(e) => Err(format!("Error parsing header: {:}", e).into()), } - app.data.error = String::new(); } pub fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult { @@ -154,14 +152,13 @@ pub fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResu #[cfg(test)] mod tests { - use jsonwebtoken::{DecodingKey, Validation}; use tui_textarea::TextArea; use super::*; - use crate::app::{utils::slurp_file, utils::strip_leading_symbol}; + use crate::app::jwt_decoder::{decode_token, DecodeArgs}; #[test] - fn test_encode_jwt_token_with_valid_payload_and_defaults() { + fn test_encode_hmac_jwt_token_with_valid_payload_and_defaults() { let mut app = App::new(250, None, "secrets".into()); app.data.encoder.payload.input = vec![ @@ -181,10 +178,21 @@ mod tests { .encoded .get_txt(), "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1MTYyMzkwMjIsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMzQ1Njc4OTAifQ.TggX4VlPVD-2G5eUT5AhzepyMCx_nuzfiQ_YkdXsMKI"); assert!(app.data.encoder.signature_verified); + + let args = DecodeArgs { + jwt: app.data.encoder.encoded.get_txt(), + secret: String::from("secrets"), + time_format_utc: false, + ignore_exp: true, + }; + + let decoded = decode_token(&args).1; + + assert!(decoded.is_ok()) } #[test] - fn test_encode_jwt_token_with_valid_payload_and_header_rs256() { + fn test_encode_rsa_jwt_token_with_valid_payload_and_header() { let mut app = App::new(250, None, "".into()); let header = vec!["{", r#" "alg": "RS256","#, r#" "typ": "JWT""#, "}"]; @@ -211,23 +219,143 @@ mod tests { assert!(app.data.encoder.signature_verified); // decode the key and verify - let mut secret_validator = Validation::new(Algorithm::RS256); - secret_validator.leeway = 1000; - secret_validator - .required_spec_claims - .retain(|claim| claim != "exp"); - secret_validator.validate_exp = false; - - let secret_string = "@./test_data/test_rsa_public_key.pem"; - - let secret = slurp_file(strip_leading_symbol(secret_string)).unwrap(); - - let decoded = jsonwebtoken::decode::( - &app.data.encoder.encoded.get_txt(), - &DecodingKey::from_rsa_pem(&secret).unwrap(), - &secret_validator, - ) - .unwrap(); + let args = DecodeArgs { + jwt: app.data.encoder.encoded.get_txt(), + secret: String::from("@./test_data/test_rsa_public_key.pem"), + time_format_utc: false, + ignore_exp: true, + }; + + let decoded = decode_token(&args).1.unwrap(); + + assert_eq!( + decoded.header, + serde_json::from_str(header.join("\n").as_str()).unwrap() + ); + assert_eq!( + decoded.claims, + serde_json::from_str(claims.join("\n").as_str()).unwrap() + ); + } + + #[test] + fn test_encode_rsa_pss_jwt_token_with_valid_payload_and_header() { + let mut app = App::new(250, None, "".into()); + + let header = vec!["{", r#" "alg": "PS256","#, r#" "typ": "JWT""#, "}"]; + app.data.encoder.header.input = header.clone().into(); + + let claims = vec![ + "{", + r#" "sub": "1234567890","#, + r#" "name": "John Doe","#, + r#" "iat": 1516239022"#, + "}", + ]; + app.data.encoder.payload.input = claims.clone().into(); + + app.data.encoder.secret.input = "@./test_data/test_rsa_private_key.der".into(); + + encode_jwt_token(&mut app); + assert_eq!(app.data.error, ""); + assert!(!app.data.encoder.encoded.get_txt().is_empty()); + assert!(app.data.encoder.signature_verified); + + // decode the key and verify + let args = DecodeArgs { + jwt: app.data.encoder.encoded.get_txt(), + secret: String::from("@./test_data/test_rsa_public_key.der"), + time_format_utc: false, + ignore_exp: true, + }; + + let decoded = decode_token(&args).1.unwrap(); + + assert_eq!( + decoded.header, + serde_json::from_str(header.join("\n").as_str()).unwrap() + ); + assert_eq!( + decoded.claims, + serde_json::from_str(claims.join("\n").as_str()).unwrap() + ); + } + + #[test] + fn test_encode_ecdsa_jwt_token_with_valid_payload_and_header() { + let mut app = App::new(250, None, "".into()); + + let header = vec!["{", r#" "alg": "ES256","#, r#" "typ": "JWT""#, "}"]; + app.data.encoder.header.input = header.clone().into(); + + let claims = vec![ + "{", + r#" "sub": "1234567890","#, + r#" "name": "John Doe","#, + r#" "iat": 1516239022"#, + "}", + ]; + app.data.encoder.payload.input = claims.clone().into(); + + app.data.encoder.secret.input = "@./test_data/test_ecdsa_private_key.pk8".into(); + + encode_jwt_token(&mut app); + assert_eq!(app.data.error, ""); + assert!(!app.data.encoder.encoded.get_txt().is_empty()); + assert!(app.data.encoder.signature_verified); + + // decode the key and verify + let args = DecodeArgs { + jwt: app.data.encoder.encoded.get_txt(), + secret: String::from("@./test_data/test_ecdsa_public_key.pk8"), + time_format_utc: false, + ignore_exp: true, + }; + + let decoded = decode_token(&args).1.unwrap(); + + assert_eq!( + decoded.header, + serde_json::from_str(header.join("\n").as_str()).unwrap() + ); + assert_eq!( + decoded.claims, + serde_json::from_str(claims.join("\n").as_str()).unwrap() + ); + } + + #[test] + fn test_encode_eddsa_jwt_token_with_valid_payload_and_header() { + let mut app = App::new(250, None, "".into()); + + let header = vec!["{", r#" "alg": "EdDSA","#, r#" "typ": "JWT""#, "}"]; + app.data.encoder.header.input = header.clone().into(); + + let claims = vec![ + "{", + r#" "sub": "1234567890","#, + r#" "name": "John Doe","#, + r#" "iat": 1516239022"#, + "}", + ]; + app.data.encoder.payload.input = claims.clone().into(); + + app.data.encoder.secret.input = "@./test_data/test_eddsa_private_key.pem".into(); + + encode_jwt_token(&mut app); + assert_eq!(app.data.error, ""); + assert!(!app.data.encoder.encoded.get_txt().is_empty()); + assert!(app.data.encoder.signature_verified); + + // decode the key and verify + let args = DecodeArgs { + jwt: app.data.encoder.encoded.get_txt(), + secret: String::from("@./test_data/test_eddsa_public_key.pem"), + time_format_utc: false, + ignore_exp: true, + }; + + let decoded = decode_token(&args).1.unwrap(); assert_eq!( decoded.header, diff --git a/test_data/test_ecdsa_private_key.pk8 b/test_data/test_ecdsa_private_key.pk8 new file mode 100644 index 0000000000000000000000000000000000000000..f838b0488205eedcefee26544541bd2d28bcf577 GIT binary patch literal 138 zcmV;50CoQ`frkPC05B5<2P%e0&OHJF1_&yKNX|V20S5$aFlzz<0R$jfF<%I2V`HYU z+NrtS>^clW)qFbm9wP6XJ)T?+$GRa3(zTyfu9smFU literal 0 HcmV?d00001 diff --git a/test_data/test_ecdsa_public_key.pk8 b/test_data/test_ecdsa_public_key.pk8 new file mode 100644 index 0000000..188c9d8 --- /dev/null +++ b/test_data/test_ecdsa_public_key.pk8 @@ -0,0 +1 @@ +ò@O%I_!IAM Ly5+\Iw[ a ԫxG2GU \ No newline at end of file