From 8993ce91721420a39ec65760278250c9c0407626 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 16 Sep 2021 16:56:16 -0400 Subject: [PATCH 1/6] add support for base64-encoded secret string --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 26 +++++++++++++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abf2128..a70f183 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,7 @@ name = "jwt-cli" version = "4.0.0" dependencies = [ "atty", + "base64 0.13.0", "bunt", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index dd7c2e4..ffc58b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ serde_json = "1" chrono = "0.4" parse_duration = "2.1.1" atty = "0.2" +base64 = "0.13.0" diff --git a/src/main.rs b/src/main.rs index a3c6264..0cb02e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use atty::Stream; +use base64::decode as base64_decode; use chrono::{TimeZone, Utc}; use clap::{arg_enum, crate_authors, crate_version, App, Arg, ArgMatches, SubCommand}; use jsonwebtoken::errors::{ErrorKind, Result as JWTResult}; @@ -235,6 +236,13 @@ fn config_options<'a, 'b>() -> App<'a, 'b> { Arg::with_name("no_iat") .help("prevent an iat claim from being automatically added") .long("no-iat") + ).arg( + Arg::with_name("base64") + .help("interpret the secret string as base64-encoded bytes") + .takes_value(false) + .long("base64") + .short("B") + .required(false), ).arg( Arg::with_name("secret") .help("the secret to sign the JWT with. Can be prefixed with @ to read from a binary file") @@ -339,12 +347,20 @@ fn slurp_file(file_name: &str) -> Vec { fs::read(file_name).unwrap_or_else(|_| panic!("Unable to read file {}", file_name)) } -fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult { +fn encoding_key_from_secret( + alg: &Algorithm, + secret_string: &str, + base64: bool, +) -> JWTResult { match alg { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { if secret_string.starts_with('@') { let secret = slurp_file(&secret_string.chars().skip(1).collect::()); Ok(EncodingKey::from_secret(&secret)) + } else if base64 { + Ok(EncodingKey::from_secret( + &base64_decode(secret_string).unwrap(), + )) } else { Ok(EncodingKey::from_secret(secret_string.as_bytes())) } @@ -469,8 +485,12 @@ fn encode_token(matches: &ArgMatches) -> JWTResult { let payloads = maybe_payloads.into_iter().flatten().collect(); let Payload(claims) = Payload::from_payloads(payloads); - encoding_key_from_secret(&algorithm, matches.value_of("secret").unwrap()) - .and_then(|secret| encode(&header, &claims, &secret)) + encoding_key_from_secret( + &algorithm, + matches.value_of("secret").unwrap(), + matches.is_present("base64"), + ) + .and_then(|secret| encode(&header, &claims, &secret)) } fn decode_token( From 5cacc1a73a4fa9b5ec7b98154963017ee1954864 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 16 Sep 2021 17:04:05 -0400 Subject: [PATCH 2/6] add test for base64 secret --- tests/jwt-cli.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/jwt-cli.rs b/tests/jwt-cli.rs index 3adca30..bafc84c 100644 --- a/tests/jwt-cli.rs +++ b/tests/jwt-cli.rs @@ -7,6 +7,7 @@ mod tests { encoding_key_from_secret, is_payload_item, is_timestamp_or_duration, translate_algorithm, OutputFormat, Payload, PayloadItem, SupportedAlgorithms, }; + use base64::{decode as base64_decode}; use chrono::{Duration, TimeZone, Utc}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, TokenData}; use serde_json::{from_value, json}; @@ -658,7 +659,15 @@ mod tests { #[test] fn encoding_key_from_secret_handles_at() { let expected = EncodingKey::from_secret(include_bytes!("hmac-key.bin")); - let key = encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin").unwrap(); + let key = encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin", false).unwrap(); + assert_eq!(expected, key); + } + + #[test] + fn encoding_key_from_secret_handles_base64() { + let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; + let expected = EncodingKey::from_secret(&base64_decode(b64).unwrap()); + let key = encoding_key_from_secret(&Algorithm::HS256, b64, true).unwrap(); assert_eq!(expected, key); } From 1365df35d21c550eda9f5479e63b8c63af4ab157 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 16 Sep 2021 17:05:37 -0400 Subject: [PATCH 3/6] format test case --- tests/jwt-cli.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/jwt-cli.rs b/tests/jwt-cli.rs index bafc84c..10315b7 100644 --- a/tests/jwt-cli.rs +++ b/tests/jwt-cli.rs @@ -7,7 +7,7 @@ mod tests { encoding_key_from_secret, is_payload_item, is_timestamp_or_duration, translate_algorithm, OutputFormat, Payload, PayloadItem, SupportedAlgorithms, }; - use base64::{decode as base64_decode}; + use base64::decode as base64_decode; use chrono::{Duration, TimeZone, Utc}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, TokenData}; use serde_json::{from_value, json}; @@ -659,7 +659,8 @@ mod tests { #[test] fn encoding_key_from_secret_handles_at() { let expected = EncodingKey::from_secret(include_bytes!("hmac-key.bin")); - let key = encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin", false).unwrap(); + let key = + encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin", false).unwrap(); assert_eq!(expected, key); } From a1a34d1cbd691d8cbff11c7dfa3c1c03bb16ed8e Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Fri, 17 Sep 2021 07:50:58 -0400 Subject: [PATCH 4/6] rework to use b64: prefix to distinguish base-64 encoded bytes --- src/main.rs | 34 ++++++++++++---------------------- tests/jwt-cli.rs | 17 ++++++++++++++--- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0cb02e1..5bb6f44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -236,16 +236,9 @@ fn config_options<'a, 'b>() -> App<'a, 'b> { Arg::with_name("no_iat") .help("prevent an iat claim from being automatically added") .long("no-iat") - ).arg( - Arg::with_name("base64") - .help("interpret the secret string as base64-encoded bytes") - .takes_value(false) - .long("base64") - .short("B") - .required(false), ).arg( Arg::with_name("secret") - .help("the secret to sign the JWT with. Can be prefixed with @ to read from a binary file") + .help("the secret to sign the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes") .takes_value(true) .long("secret") .short("S") @@ -274,7 +267,7 @@ fn config_options<'a, 'b>() -> App<'a, 'b> { .long("iso8601") ).arg( Arg::with_name("secret") - .help("the secret to validate the JWT with. Can be prefixed with @ to read from a binary file") + .help("the secret to validate the JWT with. Prefix with @ to read from a file or b64: to use base-64 encoded bytes") .takes_value(true) .long("secret") .short("S") @@ -347,19 +340,15 @@ fn slurp_file(file_name: &str) -> Vec { fs::read(file_name).unwrap_or_else(|_| panic!("Unable to read file {}", file_name)) } -fn encoding_key_from_secret( - alg: &Algorithm, - secret_string: &str, - base64: bool, -) -> JWTResult { +fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult { match alg { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { if secret_string.starts_with('@') { let secret = slurp_file(&secret_string.chars().skip(1).collect::()); Ok(EncodingKey::from_secret(&secret)) - } else if base64 { + } else if secret_string.starts_with("b64:") { Ok(EncodingKey::from_secret( - &base64_decode(secret_string).unwrap(), + &base64_decode(&secret_string.chars().skip(4).collect::()).unwrap(), )) } else { Ok(EncodingKey::from_secret(secret_string.as_bytes())) @@ -398,6 +387,11 @@ fn decoding_key_from_secret( if secret_string.starts_with('@') { let secret = slurp_file(&secret_string.chars().skip(1).collect::()); Ok(DecodingKey::from_secret(&secret).into_static()) + } else if secret_string.starts_with("b64:") { + Ok(DecodingKey::from_secret( + &base64_decode(&secret_string.chars().skip(4).collect::()).unwrap(), + ) + .into_static()) } else { Ok(DecodingKey::from_secret(secret_string.as_bytes()).into_static()) } @@ -485,12 +479,8 @@ fn encode_token(matches: &ArgMatches) -> JWTResult { let payloads = maybe_payloads.into_iter().flatten().collect(); let Payload(claims) = Payload::from_payloads(payloads); - encoding_key_from_secret( - &algorithm, - matches.value_of("secret").unwrap(), - matches.is_present("base64"), - ) - .and_then(|secret| encode(&header, &claims, &secret)) + encoding_key_from_secret(&algorithm, matches.value_of("secret").unwrap()) + .and_then(|secret| encode(&header, &claims, &secret)) } fn decode_token( diff --git a/tests/jwt-cli.rs b/tests/jwt-cli.rs index 10315b7..9316806 100644 --- a/tests/jwt-cli.rs +++ b/tests/jwt-cli.rs @@ -659,16 +659,17 @@ mod tests { #[test] fn encoding_key_from_secret_handles_at() { let expected = EncodingKey::from_secret(include_bytes!("hmac-key.bin")); - let key = - encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin", false).unwrap(); + let key = encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin").unwrap(); assert_eq!(expected, key); } #[test] fn encoding_key_from_secret_handles_base64() { let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; + let mut arg: String = "b64:".to_owned(); + arg.push_str(&b64); let expected = EncodingKey::from_secret(&base64_decode(b64).unwrap()); - let key = encoding_key_from_secret(&Algorithm::HS256, b64, true).unwrap(); + let key = encoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); assert_eq!(expected, key); } @@ -679,6 +680,16 @@ mod tests { assert_eq!(expected, key); } + #[test] + fn decoding_key_from_secret_handles_base64() { + let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; + let mut arg: String = "b64:".to_owned(); + arg.push_str(&b64); + let expected = DecodingKey::from_secret(&base64_decode(b64).unwrap()).into_static(); + let key = decoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); + assert_eq!(expected, key); + } + #[test] fn encodes_and_decodes_an_rsa_ssa_pss_token_using_key_from_file() { let body: String = "{\"field\":\"value\"}".to_string(); From fe006e5e4976b30763f44ee1c2c85b47bd00cf10 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Fri, 17 Sep 2021 07:53:57 -0400 Subject: [PATCH 5/6] remove extraneous borrow op --- tests/jwt-cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/jwt-cli.rs b/tests/jwt-cli.rs index 9316806..a654764 100644 --- a/tests/jwt-cli.rs +++ b/tests/jwt-cli.rs @@ -667,7 +667,7 @@ mod tests { fn encoding_key_from_secret_handles_base64() { let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; let mut arg: String = "b64:".to_owned(); - arg.push_str(&b64); + arg.push_str(b64); let expected = EncodingKey::from_secret(&base64_decode(b64).unwrap()); let key = encoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); assert_eq!(expected, key); @@ -684,7 +684,7 @@ mod tests { fn decoding_key_from_secret_handles_base64() { let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; let mut arg: String = "b64:".to_owned(); - arg.push_str(&b64); + arg.push_str(b64); let expected = DecodingKey::from_secret(&base64_decode(b64).unwrap()).into_static(); let key = decoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); assert_eq!(expected, key); From dadde35d411320c2653de525bd37d1a135d084eb Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Tue, 21 Sep 2021 18:18:50 -0400 Subject: [PATCH 6/6] use format macro to construct base64 args in test --- tests/jwt-cli.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/jwt-cli.rs b/tests/jwt-cli.rs index a654764..0f1df2d 100644 --- a/tests/jwt-cli.rs +++ b/tests/jwt-cli.rs @@ -666,8 +666,7 @@ mod tests { #[test] fn encoding_key_from_secret_handles_base64() { let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; - let mut arg: String = "b64:".to_owned(); - arg.push_str(b64); + let arg = format!("b64:{}", b64); let expected = EncodingKey::from_secret(&base64_decode(b64).unwrap()); let key = encoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); assert_eq!(expected, key); @@ -683,8 +682,7 @@ mod tests { #[test] fn decoding_key_from_secret_handles_base64() { let b64 = "+t0vs/PPB0dvyYKIk1DYvz5WyCUds5DLy07ycOK5oHA="; - let mut arg: String = "b64:".to_owned(); - arg.push_str(b64); + let arg = format!("b64:{}", b64); let expected = DecodingKey::from_secret(&base64_decode(b64).unwrap()).into_static(); let key = decoding_key_from_secret(&Algorithm::HS256, &arg).unwrap(); assert_eq!(expected, key);