sd_jwt_payload/
disclosure.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::Error;
5use serde_json::json;
6use serde_json::Value;
7use std::fmt::Display;
8
9/// A disclosable value.
10/// Both object properties and array elements disclosures are supported.
11///
12/// See: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-disclosures
13#[derive(Debug, Clone, Eq)]
14pub struct Disclosure {
15  /// The salt value.
16  pub salt: String,
17  /// The claim name, optional for array elements.
18  pub claim_name: Option<String>,
19  /// The claim Value which can be of any type.
20  pub claim_value: Value,
21  /// Base64Url-encoded disclosure.
22  unparsed: String,
23}
24
25impl AsRef<str> for Disclosure {
26  fn as_ref(&self) -> &str {
27    &self.unparsed
28  }
29}
30
31impl Disclosure {
32  /// Creates a new instance of [`Disclosure`].
33  ///
34  /// Use `.to_string()` to get the actual disclosure.
35  pub(crate) fn new(salt: String, claim_name: Option<String>, claim_value: Value) -> Self {
36    let string_encoded = {
37      let json_input = if let Some(name) = claim_name.as_deref() {
38        json!([salt, name, claim_value])
39      } else {
40        json!([salt, claim_value])
41      };
42
43      multibase::Base::Base64Url.encode(json_input.to_string())
44    };
45    Self {
46      salt,
47      claim_name,
48      claim_value,
49      unparsed: string_encoded,
50    }
51  }
52
53  /// Parses a Base64 encoded disclosure into a [`Disclosure`].
54  ///
55  /// ## Error
56  ///
57  /// Returns an [`Error::InvalidDisclosure`] if input is not a valid disclosure.
58  pub fn parse(disclosure: &str) -> Result<Self, Error> {
59    let decoded: Vec<Value> = multibase::Base::Base64Url
60      .decode(disclosure)
61      .map_err(|_e| {
62        Error::InvalidDisclosure(format!(
63          "Base64 decoding of the disclosure was not possible {}",
64          disclosure
65        ))
66      })
67      .and_then(|data| {
68        serde_json::from_slice(&data).map_err(|_e| {
69          Error::InvalidDisclosure(format!(
70            "decoded disclosure could not be serialized as an array {}",
71            disclosure
72          ))
73        })
74      })?;
75
76    if decoded.len() == 2 {
77      Ok(Self {
78        salt: decoded
79          .first()
80          .ok_or(Error::InvalidDisclosure("invalid salt".to_string()))?
81          .as_str()
82          .ok_or(Error::InvalidDisclosure(
83            "salt could not be parsed as a string".to_string(),
84          ))?
85          .to_owned(),
86        claim_name: None,
87        claim_value: decoded
88          .get(1)
89          .ok_or(Error::InvalidDisclosure("invalid claim name".to_string()))?
90          .clone(),
91        unparsed: disclosure.to_string(),
92      })
93    } else if decoded.len() == 3 {
94      Ok(Self {
95        salt: decoded
96          .first()
97          .ok_or(Error::InvalidDisclosure("invalid salt".to_string()))?
98          .as_str()
99          .ok_or(Error::InvalidDisclosure(
100            "salt could not be parsed as a string".to_string(),
101          ))?
102          .to_owned(),
103        claim_name: Some(
104          decoded
105            .get(1)
106            .ok_or(Error::InvalidDisclosure("invalid claim name".to_string()))?
107            .as_str()
108            .ok_or(Error::InvalidDisclosure(
109              "claim name could not be parsed as a string".to_string(),
110            ))?
111            .to_owned(),
112        ),
113        claim_value: decoded
114          .get(2)
115          .ok_or(Error::InvalidDisclosure("invalid claim name".to_string()))?
116          .clone(),
117        unparsed: disclosure.to_string(),
118      })
119    } else {
120      Err(Error::InvalidDisclosure(format!(
121        "deserialized array has an invalid length of {}",
122        decoded.len()
123      )))
124    }
125  }
126
127  pub fn as_str(&self) -> &str {
128    self.as_ref()
129  }
130}
131
132impl Display for Disclosure {
133  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134    write!(f, "{}", self.unparsed)
135  }
136}
137
138impl PartialEq for Disclosure {
139  fn eq(&self, other: &Self) -> bool {
140    self.claim_name == other.claim_name && self.salt == other.salt && self.claim_value == other.claim_value
141  }
142}
143
144#[cfg(test)]
145mod test {
146  use super::Disclosure;
147
148  // Test values from:
149  // https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#appendix-A.2-7
150  #[test]
151  fn test_parsing() {
152    let disclosure = Disclosure::new(
153      "2GLC42sKQveCfGfryNRN9w".to_string(),
154      Some("time".to_owned()),
155      "2012-04-23T18:25Z".to_owned().into(),
156    );
157
158    let parsed =
159      Disclosure::parse("WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInRpbWUiLCAiMjAxMi0wNC0yM1QxODoyNVoiXQ").unwrap();
160    assert_eq!(parsed, disclosure);
161  }
162}