use core::fmt;
use std::collections::{HashMap, HashSet};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use bherror::{
traits::{ErrorContext, ForeignError},
Error,
};
use super::{error::DecodingResult, path_map::PathMapObject, JsonNodePath, Value};
use crate::{
error::FormatError,
utils::{self},
DecodingError,
};
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Disclosure {
pub(crate) data: DisclosureData,
serialized: String,
}
impl TryFrom<String> for Disclosure {
type Error = Error<FormatError>;
fn try_from(serialized: String) -> Result<Self, Self::Error> {
let decoded = URL_SAFE_NO_PAD
.decode(&serialized)
.foreign_err(|| {
FormatError::InvalidDisclosure("provided string is not base64 ".to_string())
})
.ctx(|| serialized.clone())?;
let array: Vec<Value> = serde_json::from_slice(&decoded)
.foreign_err(|| {
FormatError::InvalidDisclosure(
"serde json could not parse decoded base64 string ".to_string(),
)
})
.ctx(|| serialized.clone())?;
let data = match array.len() {
3 => {
let [salt, key, value] = array.try_into().unwrap();
create_disclosure_data_key_value(salt, key, value)
}
2 => {
let [salt, value] = array.try_into().unwrap();
create_disclosure_data_array_element(salt, value)
}
_ => Err(Error::root(FormatError::InvalidDisclosure(format!(
"deserialized disclosure array has invalid length {}",
array.len(),
)))),
}
.ctx(|| "error while creating a disclosure from base64 serialized string ".to_string())
.ctx(|| serialized.clone())?;
Ok(Self { data, serialized })
}
}
fn create_disclosure_data_key_value(
salt: Value,
key: Value,
value: Value,
) -> crate::Result<DisclosureData, FormatError> {
let Value::String(salt) = salt else {
return Err(Error::root(FormatError::InvalidDisclosure(
"salt value is not a string".to_string(),
)));
};
let Value::String(key) = key else {
return Err(Error::root(FormatError::InvalidDisclosure(
"key value is not a string".to_string(),
)));
};
Ok(DisclosureData::KeyValue { salt, key, value })
}
fn create_disclosure_data_array_element(
salt: Value,
value: Value,
) -> crate::Result<DisclosureData, FormatError> {
let Value::String(salt) = salt else {
return Err(Error::root(FormatError::InvalidDisclosure(
"salt value is not a string".to_string(),
)));
};
Ok(DisclosureData::ArrayElement { salt, value })
}
impl fmt::Display for Disclosure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.data {
DisclosureData::KeyValue { salt, key, value } => {
write!(f, "[{}, {}, {}]", salt, key, value)
}
DisclosureData::ArrayElement { salt, value } => write!(f, "[{}, {}]", salt, value),
}
}
}
impl Disclosure {
pub fn new(salt: String, claim_name: Option<String>, claim_value: Value) -> Self {
let input = if let Some(name) = &claim_name {
format!("[\"{}\", \"{}\", {}]", &salt, &name, &claim_value)
} else {
format!("[\"{}\", {}]", &salt, &claim_value)
};
let encoded = bh_jws_utils::base64_url_encode(input);
let data = if let Some(name) = claim_name {
DisclosureData::KeyValue {
salt,
key: name,
value: claim_value,
}
} else {
DisclosureData::ArrayElement {
salt,
value: claim_value,
}
};
Self {
data,
serialized: encoded,
}
}
pub fn value(&self) -> &Value {
match &self.data {
DisclosureData::KeyValue { value, .. } => value,
DisclosureData::ArrayElement { value, .. } => value,
}
}
pub fn claim_name(&self) -> Option<&str> {
match &self.data {
DisclosureData::KeyValue { key, .. } => Some(key),
_ => None,
}
}
pub fn as_str(&self) -> &str {
&self.serialized
}
pub fn into_string(self) -> String {
self.serialized
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum DisclosureData {
KeyValue {
salt: Salt,
key: String,
value: Value,
},
ArrayElement {
salt: Salt,
value: Value,
},
}
pub type Salt = String;
pub type Digest = String;
#[derive(Debug)]
pub(crate) struct DisclosureByDigestTable<'a>(pub(crate) HashMap<Digest, &'a Disclosure>);
impl<'a> DisclosureByDigestTable<'a> {
pub(crate) fn new(
disclosures: &'a [Disclosure],
hasher: impl crate::Hasher,
) -> DecodingResult<Self> {
let mut disclosure_by_digest = HashMap::new();
for disclosure in disclosures {
let digest = utils::base64_url_digest(disclosure.as_str().as_bytes(), &hasher);
if disclosure_by_digest.insert(digest, disclosure).is_some() {
return Err(Error::root(DecodingError::DisclosureDigestCollision));
}
}
Ok(Self(disclosure_by_digest))
}
}
#[derive(Debug, yoke::Yokeable)]
pub(crate) struct DisclosureByPathTable<'model>(PathMapObject<&'model Disclosure>);
impl<'model> DisclosureByPathTable<'model> {
pub(crate) fn new(inner: PathMapObject<&'model Disclosure>) -> Self {
Self(inner)
}
pub(crate) fn disclosures_covering_paths(
&self,
paths: &[&JsonNodePath],
) -> impl Iterator<Item = &'model Disclosure> {
let mut set = HashSet::new();
for path in paths {
let _result = self.0.traverse_path(path.iter().copied(), |disclosure| {
set.insert(*disclosure);
});
}
set.into_iter()
}
}
#[cfg(test)]
mod tests {
use bh_jws_utils::base64_url_encode;
use serde_json::{json, Value};
use crate::{error::FormatError, Disclosure};
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
fn test_disclosure_encode_and_parse(
salt: &str,
claim_name: Option<&str>,
claim_value: Value,
encoded: &str,
) -> Result {
let disclosure =
Disclosure::new(salt.to_owned(), claim_name.map(str::to_owned), claim_value);
assert_eq!(disclosure.as_str(), encoded);
let parsed = Disclosure::try_from(encoded.to_owned()).unwrap();
assert_eq!(parsed, disclosure);
Ok(())
}
#[test]
fn test_disclosure_encode_and_parse_object_property() -> Result {
test_disclosure_encode_and_parse(
"_26bc4LT-ac6q2KI6cBW5es",
Some("family_name"),
Value::String("Möbius".to_owned()),
"WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0",
)
}
#[test]
fn test_disclosure_encode_array_element() -> Result {
test_disclosure_encode_and_parse(
"lklxF5jMYlGTPUovMNIvCA",
None,
Value::String("FR".to_owned()),
"WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0",
)
}
#[test]
fn invalid_disclosure_not_a_base64_string() {
let invalid_base64 = "bla";
let decoded = Disclosure::try_from(invalid_base64.to_string());
assert_eq!(
decoded.unwrap_err().error,
FormatError::InvalidDisclosure("provided string is not base64 ".to_string())
)
}
#[test]
fn invalid_disclosure_too_few_elements_in_deserialized_array() {
let input = json!(["bla"]);
let encoded = base64_url_encode(input.to_string());
let decoded = Disclosure::try_from(encoded.clone());
assert_eq!(
decoded.unwrap_err().error,
FormatError::InvalidDisclosure(
"deserialized disclosure array has invalid length 1".to_string(),
)
);
}
#[test]
fn invalid_disclosure_too_many_elements_in_deserialized_array() {
let input = json!(["bla", "bla", 5, "bla"]);
let encoded = base64_url_encode(input.to_string());
let decoded = Disclosure::try_from(encoded.clone());
assert_eq!(
decoded.unwrap_err().error,
FormatError::InvalidDisclosure(
"deserialized disclosure array has invalid length 4".to_string()
)
);
}
#[test]
fn invalid_disclosure_salt_not_a_string() {
let input = json!([{"bla": "bla"}, 10.0]);
let encoded = base64_url_encode(input.to_string());
let decoded = Disclosure::try_from(encoded.clone());
assert_eq!(
decoded.unwrap_err().error,
FormatError::InvalidDisclosure("salt value is not a string".to_string())
);
}
#[test]
fn invalid_disclosure_key_is_not_a_string() {
let input = json!(["bla", {"bla": "bla"}, 10.0]);
let encoded = base64_url_encode(input.to_string());
let decoded = Disclosure::try_from(encoded.clone());
assert_eq!(
decoded.unwrap_err().error,
FormatError::InvalidDisclosure("key value is not a string".to_string())
);
}
}