#![allow(clippy::unwrap_used, clippy::panic)]
use polyjose::header::Header;
use polyjose::json::{self, Object, Value};
use polyjose::jwk::{Jwk, JwkSet};
const SECRET: &str = "MARKERsecretMARKERsecretMARKERsecretMARKERs";
const MARKER: &str = "MARKERsecret";
fn ec_private_jwk() -> String {
format!(
r#"{{"kty":"EC","crv":"P-256","x":"Qm0_Y3iAhrDndVQ247skVXv2bFfwnTvlLUyup22HGDM","y":"MXHhtkN4vHVS87SCP6IfVsIXSeUWHsOjnuQIgzhWL6Q","d":"{SECRET}","use":"enc","kid":"1"}}"#
)
}
fn okp_private_jwk() -> String {
format!(
r#"{{"kty":"OKP","crv":"Ed25519","x":"TrgjJgt_upZmFJb7d22T8iGZ_57PzgtlGh4Tq4b-tkc","d":"{SECRET}"}}"#
)
}
fn oct_jwk() -> String {
format!(r#"{{"kty":"oct","k":"{SECRET}","alg":"HS256"}}"#)
}
fn assert_redacted<T: core::fmt::Debug>(value: &T) {
for (style, debug) in [
("{:?}", format!("{value:?}")),
("{:#?}", format!("{value:#?}")),
] {
assert!(!debug.contains(MARKER), "{style} leaks the secret: {debug}");
assert!(
debug.contains("\"<redacted>\""),
"{style} lacks the placeholder: {debug}"
);
}
}
#[test]
fn a_string_value_is_redacted() {
assert_redacted(&Value::from(SECRET));
assert_redacted(&json::parse(format!("\"{SECRET}\"").as_bytes()).unwrap());
}
#[test]
fn a_private_member_of_an_object_is_redacted_and_its_name_is_shown() {
let mut o = Object::new();
o.insert("d", SECRET);
assert_redacted(&o);
assert!(format!("{o:?}").contains("\"d\""), "{o:?}");
let v = Value::from(o);
assert_redacted(&v);
assert!(format!("{v:?}").contains("\"d\""), "{v:?}");
}
#[test]
fn strings_nested_in_arrays_and_objects_are_redacted() {
let input = format!(
r#"{{"keys":[{ec},{okp},{oct}],"deep":[[[{{"a":[{{"k":"{SECRET}"}}]}}]]],"list":["{SECRET}"]}}"#,
ec = ec_private_jwk(),
okp = okp_private_jwk(),
oct = oct_jwk(),
);
let v = json::parse(input.as_bytes()).unwrap();
assert_redacted(&v);
assert_redacted(v.as_object().unwrap());
}
#[test]
fn the_object_iterators_are_redacted() {
let o = json::parse(oct_jwk().as_bytes())
.unwrap()
.into_object()
.unwrap();
assert_redacted(&o.iter());
assert_redacted(&o.into_iter());
}
#[test]
fn a_header_with_private_keys_and_private_parameters_is_redacted() {
let input = format!(
r#"{{"alg":"ECDH-ES+A128KW","enc":"A128GCM","kid":"k1","jwk":{ec},"epk":{okp},"x-wrapped":{oct},"x-secret":"{SECRET}","x-list":[1,"{SECRET}"]}}"#,
ec = ec_private_jwk(),
okp = okp_private_jwk(),
oct = oct_jwk(),
);
let h = Header::from_json(input.as_bytes()).unwrap();
assert_redacted(&h);
let debug = format!("{h:?}");
assert!(debug.contains(r#"kid: Some("k1")"#), "{debug}");
assert!(debug.contains("ECDH-ES+A128KW"), "{debug}");
}
#[test]
fn a_header_with_only_an_oct_key_in_jwk_is_redacted() {
let input = format!(r#"{{"alg":"HS256","jwk":{oct}}}"#, oct = oct_jwk());
assert_redacted(&Header::from_json(input.as_bytes()).unwrap());
}
#[test]
fn private_jwks_and_jwk_sets_stay_redacted() {
for key in [ec_private_jwk(), okp_private_jwk(), oct_jwk()] {
assert_redacted(&Jwk::from_json(key.as_bytes()).unwrap());
}
let with_secret_member = format!(r#"{{"kty":"oct","k":"{SECRET}","x-note":"{SECRET}"}}"#);
assert_redacted(&Jwk::from_json(with_secret_member.as_bytes()).unwrap());
let set = format!(
r#"{{"keys":[{ec},{okp},{oct}],"x-secret":"{SECRET}"}}"#,
ec = ec_private_jwk(),
okp = okp_private_jwk(),
oct = oct_jwk(),
);
let set = JwkSet::from_json(set.as_bytes()).unwrap();
assert_eq!(set.len(), 3);
assert_redacted(&set);
}
use std::collections::BTreeMap;
fn dismantle(mut v: Value) {
while let Value::Array(mut items) = v {
v = items.pop().unwrap_or(Value::Null);
}
}
#[test]
fn a_value_wrapped_in_option_result_vec_and_map_is_redacted() {
let v = Value::from(SECRET);
assert_redacted(&Some(v.clone()));
assert_redacted(&Ok::<Value, ()>(v.clone()));
assert_redacted(&Err::<(), Value>(v.clone()));
assert_redacted(&vec![v.clone(), Value::Null]);
assert_redacted(&(1, v.clone()));
assert_redacted(&BTreeMap::from([("k", v)]));
let o = json::parse(oct_jwk().as_bytes())
.unwrap()
.into_object()
.unwrap();
assert_redacted(&Some(&o));
assert_redacted(&json::parse(oct_jwk().as_bytes()));
let h = Header::from_json(format!(r#"{{"jwk":{}}}"#, oct_jwk()).as_bytes());
assert_redacted(&h);
}
#[test]
fn a_jws_style_header_with_escaped_and_spaced_input_is_redacted() {
let escaped = SECRET.replacen('M', "\\u004d", 1);
let input = format!(
"{{\"typ\":\"JWT\",\r\n \"alg\":\"ES256\",\r\n \"jwk\":{{ \"kty\" : \"oct\", \"k\" : \"{escaped}\" }},\r\n \"x-d\":\"{escaped}\"}}"
);
let h = Header::from_json(input.as_bytes()).unwrap();
assert_redacted(&h);
assert!(!format!("{h:?}").contains("u004d"));
}
#[test]
fn an_oct_only_jwk_set_and_its_other_params_are_redacted() {
let set = format!(
r#"{{"keys":[{{"kty":"oct","k":"{SECRET}","kid":"a"}},{{"kty":"oct","k":"{SECRET}","kid":"b"}}],"x-s":["{SECRET}",{{"k":"{SECRET}"}}]}}"#
);
let set = JwkSet::from_json(set.as_bytes()).unwrap();
assert_eq!(set.len(), 2);
assert_redacted(&set);
assert_redacted(set.other_params());
for key in set.keys() {
assert_redacted(key);
}
}
#[test]
fn errors_from_malformed_secret_bearing_input_do_not_echo_it() {
let bad_keys = [
format!(r#"{{"kty":"oct","k":"{SECRET}!!"}}"#),
format!(r#"{{"kty":"EC","crv":"P-256","x":"AA","y":"AA","d":"{SECRET}"}}"#),
format!(r#"{{"kty":"oct","k":"{SECRET}"#),
format!(r#"{{"kty":"RSA","n":"AQAB","e":"AQAB","d":"{SECRET}"}}"#),
format!(r#"{{"kty":"{SECRET}","k":"{SECRET}"}}"#),
];
let mut errors = 0;
for bad in &bad_keys {
if let Err(e) = Jwk::from_json(bad.as_bytes()) {
errors += 1;
for text in [format!("{e:?}"), format!("{e:#?}"), e.to_string()] {
assert!(!text.contains(MARKER), "{text}");
}
}
}
assert!(
errors >= 3,
"the inputs must be rejected, got {errors} errors"
);
let bad_header = format!(r#"{{"alg":"{SECRET}","p2c":"{SECRET}","jwk":"{SECRET}"}}"#);
if let Err(e) = Header::from_json(bad_header.as_bytes()) {
for text in [format!("{e:?}"), e.to_string()] {
assert!(!text.contains(MARKER), "{text}");
}
}
let bad_json = format!(r#"{{"d":"{SECRET}","d":"{SECRET}"}}"#);
if let Err(e) = json::parse(bad_json.as_bytes()) {
for text in [format!("{e:?}"), e.to_string()] {
assert!(!text.contains(MARKER), "{text}");
}
}
}
#[test]
fn formatter_flags_do_not_reveal_a_string() {
let v = Value::from(SECRET);
for text in [
format!("{v:>80?}"),
format!("{v:<80?}"),
format!("{v:.3?}"),
format!("{v:#10?}"),
format!("{v:#x?}"),
] {
assert!(!text.contains(MARKER), "{text}");
}
}
#[test]
fn debug_prints_128_levels_in_full_and_marks_the_129th() {
let nest = |levels: usize| {
let mut v = Value::from(SECRET);
for _ in 0..levels {
v = Value::Array(vec![v]);
}
v
};
let v = nest(128);
let debug = format!("{v:?}");
assert_eq!(debug.matches("Array(").count(), 128);
assert!(debug.contains("String(\"<redacted>\")"));
assert!(!debug.contains("<too deep>") && !debug.contains(MARKER));
dismantle(v);
let v = nest(129);
let debug = format!("{v:?}");
assert_eq!(debug.matches("Array(").count(), 128);
assert_eq!(debug.matches("<too deep>").count(), 1);
assert!(
!debug.contains("String(") && !debug.contains(MARKER),
"{debug}"
);
dismantle(v);
}
#[test]
fn debug_of_a_million_deep_hand_built_value_neither_overflows_nor_leaks() {
let mut v = Value::from(SECRET);
for _ in 0..1_000_000 {
v = Value::Array(vec![v]);
}
for debug in [format!("{v:?}"), format!("{v:#?}")] {
assert_eq!(debug.matches("<too deep>").count(), 1);
assert!(!debug.contains(MARKER));
}
dismantle(v);
}
#[test]
fn numbers_and_bools_print_but_hold_no_secret() {
let v = json::parse(br#"{"exp":1700000000,"p2c":1000,"ext":true,"n":null,"f":1.5e3}"#).unwrap();
let debug = format!("{v:?}");
assert!(
debug.contains("Bool(true)") && debug.contains("Null"),
"{debug}"
);
assert!(debug.contains("Number("), "{debug}");
}