use std::collections::HashSet;
use bherror::{traits::ErrorContext, Error};
use rand_core::CryptoRngCore;
use serde_json::{json, Value};
use crate::{
issuer::IssuerError,
utils::{self, check_claim_names_object, is_reserved_key_name},
Disclosure, DisplayWrapper, Hasher, JsonNodePath, JsonNodePathSegment, JsonObject, ELLIPSIS,
RESERVED_CLAIM_NAMES, SD,
};
type Result<T> = bherror::Result<T, IssuerError>;
pub(crate) fn encode_claims<H: Hasher, R: CryptoRngCore + ?Sized>(
claims: &mut JsonObject,
disclosure_paths: &[&[JsonNodePathSegment<'_>]],
hasher: &H,
rng: &mut R,
) -> Result<Vec<Disclosure>> {
if let Some(name) = check_reserved_keys_in_claims(claims) {
return Err(Error::root(IssuerError::ReservedOrRegisteredClaimName(
name,
)));
}
check_duplicate_paths(disclosure_paths)?;
let mut disclosures = vec![];
for path in toposort_node_paths(disclosure_paths) {
let salt = generate_salt(rng);
disclosures.push(conceal_disclosure(claims, path, hasher, salt)?);
}
Ok(disclosures)
}
fn conceal_disclosure<H: Hasher>(
claims: &mut JsonObject,
path: &JsonNodePath,
hasher: &H,
salt: String,
) -> Result<Disclosure> {
check_reserved_keys_in_path(path)?;
let (last_segment, path_without_last) = path
.split_last()
.ok_or_else(|| Error::root(IssuerError::InvalidPath(DisplayWrapper(path).to_string())))?;
if path_without_last.is_empty() {
let JsonNodePathSegment::Key(key) = last_segment else {
return Err(Error::root(IssuerError::InvalidPath(
DisplayWrapper(path).to_string(),
)));
};
return conceal_disclosure_in_object(claims, key, salt, hasher, path);
}
let disclosure_parent =
crate::index_mut_object_by_path(claims, path_without_last).ok_or_else(|| {
Error::root(IssuerError::NonExistentPath(
DisplayWrapper(path).to_string(),
))
})?;
match (disclosure_parent, last_segment) {
(Value::Object(object), JsonNodePathSegment::Key(key)) => {
conceal_disclosure_in_object(object, key, salt, hasher, path)
}
(Value::Array(parent_array), JsonNodePathSegment::Index(index)) => {
conceal_disclosure_in_array(parent_array, *index, salt, hasher, path)
}
_ => Err(Error::root(IssuerError::InvalidPath(
DisplayWrapper(path).to_string(),
))),
}
}
fn conceal_disclosure_in_object<H: Hasher>(
object: &mut JsonObject,
key: &str,
salt: String,
hasher: &H,
path: &JsonNodePath,
) -> Result<Disclosure> {
let value = object.remove(key).ok_or_else(|| {
Error::root(IssuerError::NonExistentPath(
DisplayWrapper(path).to_string(),
))
})?;
let disclosure = Disclosure::new(salt, Some(key.to_owned()), value);
let digest = utils::base64_url_digest(disclosure.as_str().as_bytes(), hasher);
if let Value::Array(sd_array) = object.entry(SD).or_insert(Value::Array(vec![])) {
sd_array.push(digest.into());
} else {
return Err(
Error::root(IssuerError::ReservedOrRegisteredClaimName(SD)).ctx(format!(
"_sd value is not an array at {}",
DisplayWrapper(path)
)),
);
}
Ok(disclosure)
}
fn conceal_disclosure_in_array<H: Hasher>(
array: &mut [Value],
index: u32,
salt: String,
hasher: &H,
path: &JsonNodePath,
) -> Result<Disclosure> {
let mut value = Value::Null;
std::mem::swap(
&mut value,
array
.get_mut(index as usize)
.ok_or_else(|| {
Error::root(IssuerError::NonExistentPath(
DisplayWrapper(path).to_string(),
))
})?,
);
let disclosure = Disclosure::new(salt, None, value);
let actual_digest = utils::base64_url_digest(disclosure.as_str().as_bytes(), hasher);
array[index as usize] = json!({ELLIPSIS: Value::String(actual_digest)});
Ok(disclosure)
}
fn toposort_node_paths<'p>(
disclosure_paths: &'p [&'p JsonNodePath],
) -> impl Iterator<Item = &'p JsonNodePath<'p>> {
let mut paths = disclosure_paths.to_owned();
paths.sort_unstable_by_key(|path| path.len());
paths.into_iter().rev()
}
fn generate_salt<R: CryptoRngCore + ?Sized>(rng: &mut R) -> String {
let mut salt = [0; SALT_ENTROPY_BYTES];
rng.fill_bytes(&mut salt);
bh_jws_utils::base64_url_encode(salt)
}
fn check_duplicate_paths(disclosure_paths: &[&JsonNodePath]) -> Result<()> {
let mut uniq = HashSet::new();
for path in disclosure_paths {
if !uniq.insert(path) {
return Err(Error::root(IssuerError::DuplicatePath(
DisplayWrapper(path as &JsonNodePath).to_string(),
)));
}
}
Ok(())
}
fn check_reserved_keys_in_claims(claims: &JsonObject) -> Option<&'static str> {
check_claim_names_object(
claims,
&|claim| {
RESERVED_CLAIM_NAMES
.iter()
.find(|reserved| **reserved == claim)
.copied()
},
true,
)
}
fn check_reserved_keys_in_path(path: &[JsonNodePathSegment]) -> Result<()> {
for segment in path.iter() {
if let JsonNodePathSegment::Key(key) = segment {
if let Some(key) = is_reserved_key_name(key) {
return Err(Error::root(IssuerError::ReservedOrRegisteredClaimName(key)))
.ctx(|| format!("invalid path {0}", DisplayWrapper(path)));
}
}
}
Ok(())
}
const SALT_ENTROPY_BYTES: usize = 16;
#[cfg(test)]
pub(crate) mod tests {
use bherror::Result;
use serde_json::{json, Value};
use JsonNodePathSegment::*;
use super::encode_claims;
use crate::{
encoder::conceal_disclosure, issuer::IssuerError, json_object, utils::SD_ALG_FIELD_NAME,
JsonNodePath, JsonNodePathSegment, Sha256, ELLIPSIS, RESERVED_CLAIM_NAMES, SD,
};
fn encoder_conceal(
mut claims: Value,
salts_and_paths: Vec<(String, &JsonNodePath)>,
) -> Result<Value, IssuerError> {
let claims_object = claims.as_object_mut().unwrap();
let hasher = Sha256;
for (salt, path) in salts_and_paths {
conceal_disclosure(claims_object, path, &hasher, salt)?;
}
Ok(claims)
}
fn test_encoder_conceal(
claims: Value,
salts_and_paths: Vec<(String, &JsonNodePath)>,
expected_value: Value,
) -> Result<(), IssuerError> {
assert_eq!(
encoder_conceal(claims, salts_and_paths).unwrap(),
expected_value
);
Ok(())
}
#[test]
fn test_encoder_conceal_basic() -> Result<(), IssuerError> {
let claims = json!({
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"phone_number_verified": true,
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"updated_at": 1570000000,
"nationalities": [
"US",
"DE"
]
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
("2GLC42sKQveCfGfryNRN9w".to_string(), &[Key("given_name")]),
("eluV5Og3gSNII8EYnsxA_A".to_string(), &[Key("family_name")]),
("6Ij7tM-a5iVPGboS5tmvVA".to_string(), &[Key("email")]),
("eI8ZWm9QnKPpNPeNenHdhQ".to_string(), &[Key("phone_number")]),
(
"Qg_O64zqAxe412a108iroA".to_string(),
&[Key("phone_number_verified")],
),
("AJx-095VPrpTtN4QMOqROA".to_string(), &[Key("address")]),
("Pc33JM2LchcU_lHggv_ufQ".to_string(), &[Key("birthdate")]),
("G02NSrQfjFXQ7Io09syajA".to_string(), &[Key("updated_at")]),
(
"lklxF5jMYlGTPUovMNIvCA".to_string(),
&[Key("nationalities"), Index(0)],
),
(
"nPuoQnkRFq3BIeAm7AnXFA".to_string(),
&[Key("nationalities"), Index(1)],
),
];
let expected_value = json!({
"sub": "user_42",
"_sd": [
"jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4",
"TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo",
"JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE",
"PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI",
"XQ_3kPKt1XyX7KANkqVR6yZ2Va5NrPIvPYbyMvRKBMM",
"YavaS3viw8YSKdP8UpFfIHJfjkDtTLgrG0eCY5lgWjo", "gbOsI4Edq2x2Kw-w5wPEzakob9hV1cRD0ATN3oQL9JM",
"CrQe7S5kqBAHt-nMYXgc6bdt2SH5aTY1sU_M-PgkjPI"
],
"nationalities": [
{
"...": "pFndjkZ_VCzmyTa6UjlZo3dh-ko8aIKQc9DlGzhaVYo"
},
{
"...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0"
}
]
});
test_encoder_conceal(claims, salts_and_paths, expected_value)
}
#[test]
fn test_encoder_conceal_flat() -> Result<(), IssuerError> {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"locality": "Schulpforta",
"region": "Sachsen-Anhalt",
"country": "DE"
}
});
let salts_and_paths: Vec<(String, &JsonNodePath)> =
vec![("2GLC42sKQveCfGfryNRN9w".to_string(), &[Key("address")])];
let expected_value = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"_sd": [
"zgBGNMzh31Swh6m3LY0JZU_PdBmhsMvz69s8pv1eY54" ],
});
test_encoder_conceal(claims, salts_and_paths, expected_value)
}
#[test]
fn test_encoder_conceal_structured() -> Result<(), IssuerError> {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"locality": "Schulpforta",
"region": "Sachsen-Anhalt",
"country": "DE"
}
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Key("street_address")],
),
(
"eluV5Og3gSNII8EYnsxA_A".to_string(),
&[Key("address"), Key("locality")],
),
(
"6Ij7tM-a5iVPGboS5tmvVA".to_string(),
&[Key("address"), Key("region")],
),
(
"eI8ZWm9QnKPpNPeNenHdhQ".to_string(),
&[Key("address"), Key("country")],
),
];
let expected_value = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"_sd": [
"9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM",
"6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0",
"KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88",
"WN9r9dCBJ8HTCsS2jKASxTjEyW5m5x65_Z_2ro2jfXM",
]
},
});
test_encoder_conceal(claims, salts_and_paths, expected_value)
}
#[test]
fn test_encoder_conceal_structured_partial() -> Result<(), IssuerError> {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"locality": "Schulpforta",
"region": "Sachsen-Anhalt",
"country": "DE",
}
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Key("street_address")],
),
(
"eluV5Og3gSNII8EYnsxA_A".to_string(),
&[Key("address"), Key("locality")],
),
(
"6Ij7tM-a5iVPGboS5tmvVA".to_string(),
&[Key("address"), Key("region")],
),
];
let expected_value = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"_sd": [
"9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM",
"6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0",
"KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88",
],
"country": "DE",
},
});
test_encoder_conceal(claims, salts_and_paths, expected_value)
}
#[test]
fn test_encoder_conceal_recursive() -> Result<(), IssuerError> {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"locality": "Schulpforta",
"region": "Sachsen-Anhalt",
"country": "DE",
}
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Key("street_address")],
),
(
"eluV5Og3gSNII8EYnsxA_A".to_string(),
&[Key("address"), Key("locality")],
),
(
"6Ij7tM-a5iVPGboS5tmvVA".to_string(),
&[Key("address"), Key("region")],
),
(
"eI8ZWm9QnKPpNPeNenHdhQ".to_string(),
&[Key("address"), Key("country")],
),
("Qg_O64zqAxe412a108iroA".to_string(), &[Key("address")]),
];
let expected_value = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"_sd": [
"WBsGX2zH9ek2LlRwRzDkUMBCEa9mY7EhWNiCEE1oAqc" ],
});
test_encoder_conceal(claims, salts_and_paths, expected_value)
}
#[test]
fn non_existent_path_through_object() {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"locality": "Schulpforta",
}
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Key("non_existent_key")],
),
("Qg_O64zqAxe412a108iroA".to_string(), &[Key("address")]),
];
let error = encoder_conceal(claims, salts_and_paths).unwrap_err().error;
assert_eq!(
error,
IssuerError::NonExistentPath("$.address.non_existent_key".to_string())
);
}
#[test]
fn non_existent_path_through_array() {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": [
"street_address",
"locality",
"blabla"
]
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Index(3)],
),
("Qg_O64zqAxe412a108iroA".to_string(), &[Key("address")]),
];
assert_eq!(
encoder_conceal(claims, salts_and_paths).unwrap_err().error,
IssuerError::NonExistentPath("$.address[3]".to_string())
);
}
#[test]
fn path_ends_with_reserved_claim_name() {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"_sd": "Schulpforta",
}
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Key(SD)],
),
("Qg_O64zqAxe412a108iroA".to_string(), &[Key("address")]),
];
assert_eq!(
encoder_conceal(claims, salts_and_paths).unwrap_err().error,
IssuerError::ReservedOrRegisteredClaimName(SD)
);
}
#[test]
fn path_passes_through_reserved_claim_name() {
let claims = json!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": [
"street_address",
{
"_sd": {
"key1": "value"
}
}
]
});
let salts_and_paths: Vec<(String, &JsonNodePath)> = vec![
(
"2GLC42sKQveCfGfryNRN9w".to_string(),
&[Key("address"), Index(1), Key(SD), Key("key1")],
),
("Qg_O64zqAxe412a108iroA".to_string(), &[Key("address")]),
];
assert_eq!(
encoder_conceal(claims, salts_and_paths).unwrap_err().error,
IssuerError::ReservedOrRegisteredClaimName(SD)
);
}
#[test]
fn duplicate_paths() {
let mut claims = json_object!({
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
});
let error = encode_claims(
&mut claims,
&[&[Key("sub")], &[Key("sub")]],
&Sha256,
&mut rand::thread_rng(),
)
.unwrap_err()
.error;
assert_eq!(error, IssuerError::DuplicatePath("$.sub".to_string()));
}
#[test]
fn reserved_key_in_claims() {
for reserved_claim in RESERVED_CLAIM_NAMES {
let reserved_claim = reserved_claim.to_owned();
let mut claims = json_object!({
"ninja": "ninja",
reserved_claim: "bla"
});
let error = encode_claims(
&mut claims,
&[&[Key("ninja")]],
&Sha256,
&mut rand::thread_rng(),
)
.unwrap_err()
.error;
assert_eq!(
error,
IssuerError::ReservedOrRegisteredClaimName(reserved_claim)
);
}
let extra_test_cases = [
(json_object!({ SD: ["fake hash"] }), SD),
(
json_object!({ "a": { "b": { "c": { SD: ["fake hash"] } } } }),
SD,
),
(
json_object!({ "array": [{ ELLIPSIS: "fake hash" }] }),
ELLIPSIS,
),
(json_object!({ ELLIPSIS: "fake hash" }), ELLIPSIS),
(
json_object!({ SD_ALG_FIELD_NAME: "md5" }),
SD_ALG_FIELD_NAME,
),
];
for (mut claims, reserved_key) in extra_test_cases {
claims.insert("ninja".to_string(), Value::Bool(false));
let error = encode_claims(
&mut claims,
&[&[Key("ninja")]],
&Sha256,
&mut rand::thread_rng(),
)
.unwrap_err()
.error;
assert_eq!(
error,
IssuerError::ReservedOrRegisteredClaimName(reserved_key)
);
}
}
}