From 43a239ff3d0693aef42902b8c354b19379b39c70 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:34:03 +0100 Subject: [PATCH] Use JSON Pointer for `conceal` (#10) * use json-pointer in encoder * fix decoding bug, improve example + README * bump to version 0.2.0 * clippy fix * cleanup * fix --- CHANGELOG.md | 18 +++ Cargo.toml | 5 +- README.md | 26 ++-- examples/sd_jwt.rs | 23 ++-- src/decoder.rs | 27 ++-- src/encoder.rs | 316 +++++++++++++++++++++------------------------ src/error.rs | 3 - tests/api_test.rs | 33 +++-- 8 files changed, 233 insertions(+), 218 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9430a30 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Change Log + +## [0.2.0] + +### Added +- `HEADER_TYP` constant. + +### Changed +- Changed `SdObjectEncoder::conceal` to take a JSON pointer string, instead of a string array. + +### Removed +- Removed `SdObjectEncoder::conceal_array_entry` (replaced by `SdObjectEncoder::conceal`). + +### Fixed +- Decoding bug when objects inside arrays include digests and plain text values. + +## [0.1.2] +- 07 Draft implementation. diff --git a/Cargo.toml b/Cargo.toml index b201fcd..29d249f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sd-jwt-payload" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["IOTA Stiftung"] homepage = "https://www.iota.org" @@ -16,10 +16,11 @@ multibase = { version = "0.9", default-features = false, features = ["std"] } serde_json = { version = "1.0", default-features = false, features = ["std" ] } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } thiserror = { version = "1.0", default-features = false } -strum = { version = "0.25", default-features = false, features = ["std", "derive"] } +strum = { version = "0.26", default-features = false, features = ["std", "derive"] } itertools = { version = "0.12", default-features = false, features = ["use_std"] } iota-crypto = { version = "0.23", default-features = false, features = ["sha"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } +json-pointer = "0.3.4" [dev-dependencies] josekit = "0.8.4" diff --git a/README.md b/README.md index 67e2ae1..5016cc2 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Include the library in your `cargo.toml`. ```bash [dependencies] -sd-jwt-payload = { version = "0.1.2" } +sd-jwt-payload = { version = "0.2.0" } ``` ## Examples @@ -93,18 +93,19 @@ Any JSON object can be encoded ```rust - let mut encoder = SdObjectEncoder::try_from(object).unwrap(); + let mut encoder: SdObjectEncoder = object.try_into()?; ``` This creates a stateful encoder with `Sha-256` hash function by default to create disclosure digests. *Note: `SdObjectEncoder` is generic over `Hasher` which allows custom encoding with other hash functions.* -The encoder can encode any of the object's values or any array element, using the `conceal` and `conceal_array_entry` methods respectively. Suppose the value of `street_address` should be selectively disclosed as well as the value of `address` and the first `phone` value. +The encoder can encode any of the object's values or array elements, using the `conceal` method. Suppose the value of `street_address` should be selectively disclosed as well as the value of `address` and the first `phone` value. + ```rust - let disclosure1 = encoder.conceal(&["address", "street_address"], None).unwrap(); - let disclosure2 = encoder.conceal(&["address"], None).unwrap(); - let disclosure3 = encoder.conceal_array_entry(&["phone"], 0, None).unwrap(); + let disclosure1 = encoder.conceal("/address/street_address"], None)?; + let disclosure2 = encoder.conceal("/address", None)?; + let disclosure3 = encoder.conceal("/phone/0", None)?; ``` ``` @@ -112,11 +113,14 @@ The encoder can encode any of the object's values or any array element, using th "WyJVVXVBelg5RDdFV1g0c0FRVVM5aURLYVp3cU13blUiLCAiYWRkcmVzcyIsIHsicmVnaW9uIjoiQW55c3RhdGUiLCJfc2QiOlsiaHdiX2d0eG01SnhVbzJmTTQySzc3Q194QTUxcmkwTXF0TVVLZmI0ZVByMCJdfV0" "WyJHRDYzSTYwUFJjb3dvdXJUUmg4OG5aM1JNbW14YVMiLCAiKzQ5IDEyMzQ1NiJd" ``` +*Note: the `conceal` method takes a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) to determine the element to conceal inside the JSON object.* + + The encoder also supports adding decoys. For instance, the amount of phone numbers and the amount of claims need to be hidden. ```rust - encoder.add_decoys(&["phone"], 3).unwrap(); //Adds 3 decoys to the array `phone`. - encoder.add_decoys(&[], 6).unwrap(); // Adds 6 decoys to the top level object. + encoder.add_decoys("/phone", 3).unwrap(); //Adds 3 decoys to the array `phone`. + encoder.add_decoys("", 6).unwrap(); // Adds 6 decoys to the top level object. ``` Add the hash function claim. @@ -124,7 +128,7 @@ Add the hash function claim. encoder.add_sd_alg_property(); // This adds "_sd_alg": "sha-256" ``` -Now `encoder.object()` will return the encoded object. +Now `encoder.object()?` will return the encoded object. ```json { @@ -182,10 +186,10 @@ Parse the SD-JWT string to extract the JWT and the disclosures in order to decod *Note: Validating the signature of the JWT and extracting the claim set is outside the scope of this library. ```rust - let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string).unwrap(); + let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string)?; let claims_set: // extract claims from `sd_jwt.jwt`. let decoder = SdObjectDecoder::new(); - let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures).unwrap(); + let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures)?; ``` `decoded_object`: diff --git a/examples/sd_jwt.rs b/examples/sd_jwt.rs index 6bdf90a..31e3cb0 100644 --- a/examples/sd_jwt.rs +++ b/examples/sd_jwt.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use std::error::Error; @@ -11,6 +11,7 @@ use sd_jwt_payload::Disclosure; use sd_jwt_payload::SdJwt; use sd_jwt_payload::SdObjectDecoder; use sd_jwt_payload::SdObjectEncoder; +use sd_jwt_payload::HEADER_TYP; use serde_json::json; fn main() -> Result<(), Box> { @@ -37,23 +38,27 @@ fn main() -> Result<(), Box> { let mut encoder: SdObjectEncoder = object.try_into()?; let disclosures: Vec = vec![ - encoder.conceal(&["email"], None)?, - encoder.conceal(&["phone_number"], None)?, - encoder.conceal(&["address", "street_address"], None)?, - encoder.conceal(&["address"], None)?, - encoder.conceal_array_entry(&["nationalities"], 0, None)?, + encoder.conceal("/email", None)?, + encoder.conceal("/phone_number", None)?, + encoder.conceal("/address/street_address", None)?, + encoder.conceal("/address", None)?, + encoder.conceal("/nationalities/0", None)?, ]; + + encoder.add_decoys("/nationalities", 3)?; + encoder.add_decoys("", 4)?; // Add decoys to the top level. + encoder.add_sd_alg_property(); - println!("encoded object: {}", serde_json::to_string_pretty(encoder.object())?); + println!("encoded object: {}", serde_json::to_string_pretty(encoder.object()?)?); // Create the JWT. // Creating JWTs is outside the scope of this library, josekit is used here as an example. let mut header = JwsHeader::new(); - header.set_token_type("sd-jwt"); + header.set_token_type(HEADER_TYP); // Use the encoded object as a payload for the JWT. - let payload = JwtPayload::from_map(encoder.object().clone())?; + let payload = JwtPayload::from_map(encoder.object()?.clone())?; let key = b"0123456789ABCDEF0123456789ABCDEF"; let signer = HS256.signer_from_bytes(key)?; let jwt = jwt::encode_with_signer(&payload, &header, &signer)?; diff --git a/src/decoder.rs b/src/decoder.rs index 404a42a..375462c 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use crate::ARRAY_DIGEST_KEY; @@ -15,7 +15,7 @@ use serde_json::Map; use serde_json::Value; use std::collections::BTreeMap; -/// Substitutes digests in an SD-JWT object by their corresponding plaintext values provided by disclosures. +/// Substitutes digests in an SD-JWT object by their corresponding plain text values provided by disclosures. pub struct SdObjectDecoder { hashers: BTreeMap>, } @@ -54,7 +54,7 @@ impl SdObjectDecoder { } /// Decodes an SD-JWT `object` containing by Substituting the digests with their corresponding - /// plaintext values provided by `disclosures`. + /// plain text values provided by `disclosures`. /// /// ## Notes /// * The hasher is determined by the `_sd_alg` property. If none is set, the sha-256 hasher will @@ -227,6 +227,7 @@ impl SdObjectDecoder { } else { let decoded_object = self.decode_object(object, disclosures, processed_digests)?; output.push(Value::Object(decoded_object)); + break; } } } else if let Some(arr) = value.as_array() { @@ -265,12 +266,16 @@ mod test { "id": "did:value", }); let mut encoder = SdObjectEncoder::try_from(object).unwrap(); - let dis = encoder.conceal(&["id"], None).unwrap(); + let dis = encoder.conceal("/id", None).unwrap(); encoder - .object_mut() + .object + .as_object_mut() + .unwrap() .insert("id".to_string(), Value::String("id-value".to_string())); let decoder = SdObjectDecoder::new_with_sha256(); - let decoded = decoder.decode(encoder.object(), &vec![dis.to_string()]).unwrap_err(); + let decoded = decoder + .decode(encoder.object().unwrap(), &vec![dis.to_string()]) + .unwrap_err(); assert!(matches!(decoded, Error::ClaimCollisionError(_))); } @@ -284,9 +289,9 @@ mod test { }); let mut encoder = SdObjectEncoder::try_from(object).unwrap(); encoder.add_sd_alg_property(); - assert_eq!(encoder.object().get("_sd_alg").unwrap(), "sha-256"); + assert_eq!(encoder.object().unwrap().get("_sd_alg").unwrap(), "sha-256"); let decoder = SdObjectDecoder::new_with_sha256(); - let decoded = decoder.decode(encoder.object(), &vec![]).unwrap(); + let decoded = decoder.decode(encoder.object().unwrap(), &vec![]).unwrap(); assert!(decoded.get("_sd_alg").is_none()); } @@ -296,7 +301,7 @@ mod test { "id": "did:value", }); let mut encoder = SdObjectEncoder::try_from(object).unwrap(); - let dislosure: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap(); + let dislosure: Disclosure = encoder.conceal("/id", Some("test".to_string())).unwrap(); // 'obj' contains digest of `id` twice. let obj = json!({ "_sd":[ @@ -317,8 +322,8 @@ mod test { "tst": "tst-value" }); let mut encoder = SdObjectEncoder::try_from(object).unwrap(); - let disclosure_1: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap(); - let disclosure_2: Disclosure = encoder.conceal(&["tst"], Some("test".to_string())).unwrap(); + let disclosure_1: Disclosure = encoder.conceal("/id", Some("test".to_string())).unwrap(); + let disclosure_2: Disclosure = encoder.conceal("/tst", Some("test".to_string())).unwrap(); // 'obj' contains only the digest of `id`. let obj = json!({ "_sd":[ diff --git a/src/encoder.rs b/src/encoder.rs index ad391d5..66a88fd 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use super::Disclosure; @@ -7,6 +7,7 @@ use super::Hasher; use super::Sha256Hasher; use crate::Error; use crate::Result; +use json_pointer::JsonPointer; use rand::Rng; use serde_json::json; use serde_json::Map; @@ -16,18 +17,19 @@ pub(crate) const DIGESTS_KEY: &str = "_sd"; pub(crate) const ARRAY_DIGEST_KEY: &str = "..."; pub(crate) const DEFAULT_SALT_SIZE: usize = 30; pub(crate) const SD_ALG: &str = "_sd_alg"; +pub const HEADER_TYP: &str = "sd-jwt"; /// Transforms a JSON object into an SD-JWT object by substituting selected values /// with their corresponding disclosure digests. #[cfg(not(feature = "sha"))] pub struct SdObjectEncoder { /// The object in JSON format. - object: Map, + pub(crate) object: Value, /// Size of random data used to generate the salts for disclosures in bytes. /// Constant length for readability considerations. - salt_size: usize, + pub(crate) salt_size: usize, /// The hash function used to create digests. - hasher: H, + pub(crate) hasher: H, } /// Transforms a JSON object into an SD-JWT object by substituting selected values @@ -36,12 +38,12 @@ pub struct SdObjectEncoder { #[derive(Debug, Clone)] pub struct SdObjectEncoder { /// The object in JSON format. - object: Map, + pub(crate) object: Value, /// Size of random data used to generate the salts for disclosures in bytes. /// Constant length for readability considerations. - salt_size: usize, + pub(crate) salt_size: usize, /// The hash function used to create digests. - hasher: H, + pub(crate) hasher: H, } #[cfg(feature = "sha")] @@ -51,8 +53,13 @@ impl SdObjectEncoder { /// ## Error /// Returns [`Error::DeserializationError`] if `object` is not a valid JSON object. pub fn new(object: &str) -> Result> { + let object: Value = serde_json::from_str(object).map_err(|e| Error::DeserializationError(e.to_string()))?; + if !object.is_object() { + return Err(Error::DataTypeMismatch("expected object".to_owned())); + } + Ok(SdObjectEncoder { - object: serde_json::from_str(object).map_err(|e| Error::DeserializationError(e.to_string()))?, + object, salt_size: DEFAULT_SALT_SIZE, hasher: Sha256Hasher::new(), }) @@ -63,32 +70,36 @@ impl SdObjectEncoder { /// ## Error /// Returns [`Error::DeserializationError`] if `object` can not be serialized into a valid JSON object. pub fn try_from_serializable(object: T) -> std::result::Result { - let value: String = serde_json::to_string(&object).map_err(|e| Error::DeserializationError(e.to_string()))?; - Self::new(&value) + let object: Value = serde_json::to_value(&object).map_err(|e| Error::DeserializationError(e.to_string()))?; + SdObjectEncoder::try_from(object) } } #[cfg(feature = "sha")] impl TryFrom for SdObjectEncoder { type Error = crate::Error; - fn try_from(value: Value) -> std::result::Result { - match value { - Value::Object(object) => Ok(SdObjectEncoder { - object, - salt_size: DEFAULT_SALT_SIZE, - hasher: Sha256Hasher::new(), - }), - _ => Err(Error::DataTypeMismatch("expected object".to_owned())), + if !value.is_object() { + return Err(Error::DataTypeMismatch("expected object".to_owned())); } + + Ok(SdObjectEncoder { + object: value, + salt_size: DEFAULT_SALT_SIZE, + hasher: Sha256Hasher::new(), + }) } } impl SdObjectEncoder { /// Creates a new [`SdObjectEncoder`] with custom hash function to create digests. pub fn with_custom_hasher(object: &str, hasher: H) -> Result { + let object: Value = serde_json::to_value(object).map_err(|e| Error::DeserializationError(e.to_string()))?; + if !object.is_object() { + return Err(Error::DataTypeMismatch("expected object".to_owned())); + } Ok(Self { - object: serde_json::from_str(object).map_err(|e| Error::DeserializationError(e.to_string()))?, + object, salt_size: DEFAULT_SALT_SIZE, hasher, }) @@ -97,121 +108,94 @@ impl SdObjectEncoder { /// Substitutes a value with the digest of its disclosure. /// If no salt is provided, the disclosure will be created with a random salt value. /// - /// The value of the key specified in `path` will be concealed. E.g. for path - /// `["claim", "subclaim"]` the value of `claim.subclaim` will be concealed. + /// `path` indicates the pointer to the value that will be concealed using the syntax of + /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). /// - /// ## Error - /// [`Error::InvalidPath`] if path is invalid or the path slice is empty. - /// [`Error::DataTypeMismatch`] if existing SD format is invalid. /// - /// ## Note - /// Use `conceal_array_entry` for values in arrays. - pub fn conceal(&mut self, path: &[&str], salt: Option) -> Result { - // Error if path is not provided. - if path.is_empty() { - return Err(Error::InvalidPath("the provided path length is 0".to_string())); - } - - // Determine salt. - let salt = salt.unwrap_or(Self::gen_rand(self.salt_size)); - - // Obtain the parent of the property specified by the provided path. - let (target_key, parent_value) = Self::get_target_property_and_its_parent(&mut self.object, path)?; - - // Remove the value from the parent and create a disclosure for it. - let disclosure = Disclosure::new( - salt, - Some(target_key.to_owned()), - parent_value - .remove(target_key) - .ok_or(Error::InvalidPath(format!("{} does not exist", target_key)))?, - ); - - // Hash the disclosure. - let hash = self.hasher.encoded_digest(disclosure.as_str()); - - // Add the hash to the "_sd" array if exists; otherwise, create the array and insert the hash. - Self::add_digest_to_object(parent_value, hash)?; - Ok(disclosure) - } - - /// Substitutes a value within an array with the digest of its disclosure. - /// If no salt is provided, the disclosure will be created with random salt value. - /// - /// `path` is used to specify the array in the object, while `element_index` specifies - /// the index of the element to be concealed (index start at 0). + /// ## Example + /// ``` + /// use sd_jwt_payload::SdObjectEncoder; + /// use sd_jwt_payload::json; /// + /// let obj = json!({ + /// "id": "did:value", + /// "claim1": { + /// "abc": true + /// }, + /// "claim2": ["val_1", "val_2"] + /// }); + /// let mut encoder = SdObjectEncoder::try_from(obj).unwrap(); + /// encoder.conceal("/id", None).unwrap(); //conceals "id": "did:value" + /// encoder.conceal("/claim1/abc", None).unwrap(); //"abc": true + /// encoder.conceal("/claim2/0", None).unwrap(); //conceals "val_1" + /// ``` + /// /// ## Error - /// [`Error::InvalidPath`] if path is invalid or the path slice is empty. - /// [`Error::DataTypeMismatch`] if existing SD format is invalid. - /// [`Error::IndexOutofBounds`] if `element_index` is out of bounds. - pub fn conceal_array_entry( - &mut self, - path: &[&str], - element_index: usize, - salt: Option, - ) -> Result { - // Error if path is not provided. - if path.is_empty() { - return Err(Error::InvalidPath("the provided path length is 0".to_string())); - } - + /// * [`Error::InvalidPath`] if pointer is invalid. + /// * [`Error::DataTypeMismatch`] if existing SD format is invalid. + pub fn conceal(&mut self, path: &str, salt: Option) -> Result { // Determine salt. let salt = salt.unwrap_or(Self::gen_rand(self.salt_size)); - // Obtain the parent of the property specified by the provided path. - let (target_key, parent_value) = Self::get_target_property_and_its_parent(&mut self.object, path)?; - - let array = parent_value - .get_mut(target_key) - .ok_or(Error::InvalidPath(format!("{} does not exist", target_key)))? - .as_array_mut() - .ok_or(Error::InvalidPath(format!("{} is not an array", target_key)))?; - - // Get array element, calculate digest of the disclosure and replace the element with the object - // of form "{"...": ""}". - if let Some(element_value) = array.get_mut(element_index) { - let disclosure = Disclosure::new(salt, None, element_value.clone()); - let hash = self.hasher.encoded_digest(disclosure.as_str()); - let tripledot = json!({ARRAY_DIGEST_KEY: hash}); - *element_value = tripledot; - Ok(disclosure) - } else { - Err(Error::IndexOutofBounds(element_index)) - } - } - - fn get_target_property_and_its_parent<'a, 'b>( - json: &'a mut Map, - path: &'b [&str], - ) -> Result<(&'b str, &'a mut Map)> { - let mut parent_value = json; - let mut target_property = path[0]; - for index in 1..path.len() { - match parent_value - .get(target_property) - .ok_or(Error::InvalidPath(format!("{} does not exist", target_property)))? - { - Value::Object(_) => { - parent_value = parent_value - .get_mut(path[index - 1]) - .ok_or(Error::InvalidPath(format!("{} does not exist", path[index - 1])))? - .as_object_mut() - .ok_or(Error::InvalidPath(format!("{} is not an object", path[index - 1])))?; - target_property = path[index]; - } - _ => return Err(Error::InvalidPath(format!("{} is not an object", target_property))), + let element_pointer = path + .parse::>() + .map_err(|err| Error::InvalidPath(format!("{:?}", err)))?; + + let mut parent_pointer = element_pointer.clone(); + let element_key = parent_pointer + .pop() + .ok_or(Error::InvalidPath("path does not contain any values".to_string()))?; + + let parent = parent_pointer + .get(&self.object) + .map_err(|err| Error::InvalidPath(format!("{:?}", err)))?; + + match parent { + Value::Object(_) => { + let parent = parent_pointer + .get_mut(&mut self.object) + .map_err(|err| Error::InvalidPath(format!("{:?}", err)))? + .as_object_mut() + .ok_or(Error::InvalidPath("path does not contain any values".to_string()))?; + + // Remove the value from the parent and create a disclosure for it. + let disclosure = Disclosure::new( + salt, + Some(element_key.to_owned()), + parent + .remove(&element_key) + .ok_or(Error::InvalidPath(format!("{} does not exist", element_key)))?, + ); + + // Hash the disclosure. + let hash = self.hasher.encoded_digest(disclosure.as_str()); + + // Add the hash to the "_sd" array if exists; otherwise, create the array and insert the hash. + Self::add_digest_to_object(parent, hash)?; + Ok(disclosure) } + Value::Array(_) => { + let element = element_pointer.get_mut(&mut self.object).unwrap(); + let disclosure = Disclosure::new(salt, None, element.clone()); + let hash = self.hasher.encoded_digest(disclosure.as_str()); + let tripledot = json!({ARRAY_DIGEST_KEY: hash}); + *element = tripledot; + Ok(disclosure) + } + _ => Err(crate::Error::Unspecified( + "parent of element can can only be an object or an array".to_string(), + )), } - Ok((target_property, parent_value)) } /// Adds the `_sd_alg` property to the top level of the object. /// The value is taken from the [`crate::Hasher::alg_name`] implementation. pub fn add_sd_alg_property(&mut self) -> Option { - self - .object - .insert(SD_ALG.to_string(), Value::String(self.hasher.alg_name().to_string())) + if let Some(object) = self.object.as_object_mut() { + object.insert(SD_ALG.to_string(), Value::String(self.hasher.alg_name().to_string())) + } else { + None // Should be unreachable since the `self.object` is checked to be an object on creation. + } } /// Returns the modified object as a string. @@ -221,41 +205,40 @@ impl SdObjectEncoder { } /// Adds a decoy digest to the specified path. - /// If path is an empty slice, decoys will be added to the top level. - pub fn add_decoys(&mut self, path: &[&str], number_of_decoys: usize) -> Result<()> { + /// + /// `path` indicates the pointer to the value that will be concealed using the syntax of + /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + /// + /// Use `path` = "" to add decoys to the top level. + pub fn add_decoys(&mut self, path: &str, number_of_decoys: usize) -> Result<()> { for _ in 0..number_of_decoys { self.add_decoy(path)?; } Ok(()) } - fn add_decoy(&mut self, path: &[&str]) -> Result { - if path.is_empty() { + fn add_decoy(&mut self, path: &str) -> Result { + let mut element_pointer = path + .parse::>() + .map_err(|err| Error::InvalidPath(format!("{:?}", err)))?; + + let value = element_pointer + .get_mut(&mut self.object) + .map_err(|err| Error::InvalidPath(format!("{:?}", err)))?; + if let Some(object) = value.as_object_mut() { let (disclosure, hash) = Self::random_digest(&self.hasher, self.salt_size, true); - Self::add_digest_to_object(&mut self.object, hash)?; + Self::add_digest_to_object(object, hash)?; + Ok(disclosure) + } else if let Some(array) = value.as_array_mut() { + let (disclosure, hash) = Self::random_digest(&self.hasher, self.salt_size, true); + let tripledot = json!({ARRAY_DIGEST_KEY: hash}); + array.push(tripledot); Ok(disclosure) } else { - let (target_key, parent_value) = Self::get_target_property_and_its_parent(&mut self.object, path)?; - - let value: &mut Value = parent_value - .get_mut(target_key) - .ok_or(Error::InvalidPath(format!("{} does not exist", target_key)))?; - - if let Some(object) = value.as_object_mut() { - let (disclosure, hash) = Self::random_digest(&self.hasher, self.salt_size, true); - Self::add_digest_to_object(object, hash)?; - Ok(disclosure) - } else if let Some(array) = value.as_array_mut() { - let (disclosure, hash) = Self::random_digest(&self.hasher, self.salt_size, true); - let tripledot = json!({ARRAY_DIGEST_KEY: hash}); - array.push(tripledot); - Ok(disclosure) - } else { - Err(Error::InvalidPath(format!( - "{} is neither an object nor an array", - target_key - ))) - } + Err(Error::InvalidPath(format!( + "{:?} is neither an object nor an array", + element_pointer.pop() + ))) } } @@ -300,14 +283,11 @@ impl SdObjectEncoder { } /// Returns a reference to the internal object. - pub fn object(&self) -> &Map { - &self.object - } - - /// Returns a mutable reference to the internal object. - #[cfg(test)] - pub(crate) fn object_mut(&mut self) -> &mut Map { - &mut self.object + pub fn object(&self) -> Result<&Map> { + // Safety: encoder can be constructed from objects only. + self.object.as_object().ok_or(Error::DataTypeMismatch( + "encoder initialized with invalid JSON object".to_string(), + )) } /// Returns the used salt length. @@ -357,11 +337,11 @@ mod test { #[test] fn simple() { let mut encoder = SdObjectEncoder::try_from(object()).unwrap(); - encoder.conceal(&["claim1", "abc"], None).unwrap(); - encoder.conceal(&["id"], None).unwrap(); - encoder.add_decoys(&[], 10).unwrap(); - encoder.add_decoys(&["claim2"], 10).unwrap(); - assert!(encoder.object().get("id").is_none()); + encoder.conceal("/claim1/abc", None).unwrap(); + encoder.conceal("/id", None).unwrap(); + encoder.add_decoys("", 10).unwrap(); + encoder.add_decoys("/claim2", 10).unwrap(); + assert!(encoder.object().unwrap().get("id").is_none()); assert_eq!(encoder.object.get("_sd").unwrap().as_array().unwrap().len(), 11); assert_eq!(encoder.object.get("claim2").unwrap().as_array().unwrap().len(), 12); } @@ -369,10 +349,10 @@ mod test { #[test] fn errors() { let mut encoder = SdObjectEncoder::try_from(object()).unwrap(); - encoder.conceal(&["claim1", "abc"], None).unwrap(); + encoder.conceal("/claim1/abc", None).unwrap(); assert!(matches!( - encoder.conceal_array_entry(&["claim2"], 2, None).unwrap_err(), - Error::IndexOutofBounds(2) + encoder.conceal("claim2/2", None).unwrap_err(), + Error::InvalidPath(_) )); } @@ -380,11 +360,11 @@ mod test { fn test_wrong_path() { let mut encoder = SdObjectEncoder::try_from(object()).unwrap(); assert!(matches!( - encoder.conceal(&["claim12"], None).unwrap_err(), + encoder.conceal("/claim12", None).unwrap_err(), Error::InvalidPath(_) )); assert!(matches!( - encoder.conceal_array_entry(&["claim12"], 0, None).unwrap_err(), + encoder.conceal("/claim12/0", None).unwrap_err(), Error::InvalidPath(_) )); } @@ -396,10 +376,10 @@ mod test { claim2: vec!["arr-value1".to_string(), "arr-vlaue2".to_string()], }; let mut encoder = SdObjectEncoder::try_from_serializable(test_value).unwrap(); - encoder.conceal(&["id"], None).unwrap(); - encoder.add_decoys(&[], 10).unwrap(); - encoder.add_decoys(&["claim2"], 10).unwrap(); - assert!(encoder.object().get("id").is_none()); + encoder.conceal("/id", None).unwrap(); + encoder.add_decoys("", 10).unwrap(); + encoder.add_decoys("/claim2", 10).unwrap(); + assert!(encoder.object.get("id").is_none()); assert_eq!(encoder.object.get("_sd").unwrap().as_array().unwrap().len(), 11); assert_eq!(encoder.object.get("claim2").unwrap().as_array().unwrap().len(), 12); } diff --git a/src/error.rs b/src/error.rs index b4abe8c..cf66bd2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,9 +31,6 @@ pub enum Error { #[error("invalid input")] DeserializationError(String), - #[error("index {0} is out of bounds for the provided array")] - IndexOutofBounds(usize), - #[error("{0}")] Unspecified(String), diff --git a/tests/api_test.rs b/tests/api_test.rs index 7671f19..a48b7cc 100644 --- a/tests/api_test.rs +++ b/tests/api_test.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use josekit::jws::JwsAlgorithm; @@ -70,27 +70,27 @@ fn test_complex_structure() { let mut disclosures: Vec = vec![]; let mut encoder = SdObjectEncoder::try_from(object.clone()).unwrap(); - let disclosure = encoder.conceal(&["verified_claims", "verification", "time"], None); + let disclosure = encoder.conceal("/verified_claims/verification/time", None); disclosures.push(disclosure.unwrap()); - let disclosure = encoder.conceal_array_entry(&["verified_claims", "verification", "evidence"], 0, None); + let disclosure = encoder.conceal("/verified_claims/verification/evidence/0/document/type", None); disclosures.push(disclosure.unwrap()); - let disclosure = encoder.conceal_array_entry(&["verified_claims", "verification", "evidence"], 1, None); + let disclosure = encoder.conceal("/verified_claims/verification/evidence/1", None); disclosures.push(disclosure.unwrap()); - let disclosure = encoder.conceal(&["verified_claims", "verification", "evidence"], None); + let disclosure = encoder.conceal("/verified_claims/verification/evidence", None); disclosures.push(disclosure.unwrap()); - let disclosure = encoder.conceal(&["verified_claims", "claims", "place_of_birth", "locality"], None); + let disclosure = encoder.conceal("/verified_claims/claims/place_of_birth/locality", None); disclosures.push(disclosure.unwrap()); - let disclosure = encoder.conceal(&["verified_claims", "claims"], None); + let disclosure = encoder.conceal("/verified_claims/claims", None); disclosures.push(disclosure.unwrap()); println!( "encoded object: {}", - serde_json::to_string_pretty(&encoder.object()).unwrap() + serde_json::to_string_pretty(&encoder.object().unwrap()).unwrap() ); // Create the JWT. // Creating JWTs is out of the scope of this library, josekit is used here as an example @@ -98,7 +98,7 @@ fn test_complex_structure() { header.set_token_type("SD-JWT"); // Use the encoded object as a payload for the JWT. - let payload = JwtPayload::from_map(encoder.object().clone()).unwrap(); + let payload = JwtPayload::from_map(encoder.object().unwrap().clone()).unwrap(); let key = b"0123456789ABCDEF0123456789ABCDEF"; let signer = HS256.signer_from_bytes(key).unwrap(); let jwt = jwt::encode_with_signer(&payload, &header, &signer).unwrap(); @@ -119,6 +119,11 @@ fn test_complex_structure() { // Decode the payload by providing the disclosures that were parsed from the SD-JWT. let decoder = SdObjectDecoder::new_with_sha256(); + + println!( + "claims: {}", + serde_json::to_string_pretty(payload.claims_set()).unwrap() + ); let decoded = decoder.decode(payload.claims_set(), &sd_jwt.disclosures).unwrap(); println!("decoded object: {}", serde_json::to_string_pretty(&decoded).unwrap()); assert_eq!(Value::Object(decoded), object); @@ -131,13 +136,13 @@ fn concealed_object_in_array() { "test1": 123, }); let mut encoder = SdObjectEncoder::try_from(nested_object.clone()).unwrap(); - let disclosure = encoder.conceal(&["test1"], None); + let disclosure = encoder.conceal("/test1", None); disclosures.push(disclosure.unwrap()); let object = json!({ "test2": [ "value1", - encoder.object() + encoder.object().unwrap() ] }); @@ -150,9 +155,9 @@ fn concealed_object_in_array() { ] }); let mut encoder = SdObjectEncoder::try_from(object.clone()).unwrap(); - let disclosure = encoder.conceal_array_entry(&["test2"], 0, None); + let disclosure = encoder.conceal("/test2/0", None); disclosures.push(disclosure.unwrap()); - let disclosure = encoder.conceal(&["test2"], None); + let disclosure = encoder.conceal("/test2", None); disclosures.push(disclosure.unwrap()); let disclosures: Vec = disclosures @@ -160,7 +165,7 @@ fn concealed_object_in_array() { .map(|disclosure| disclosure.to_string()) .collect(); let decoder = SdObjectDecoder::new_with_sha256(); - let decoded = decoder.decode(encoder.object(), &disclosures).unwrap(); + let decoded = decoder.decode(encoder.object().unwrap(), &disclosures).unwrap(); assert_eq!(Value::Object(decoded), expected); }