baseid_sd_jwt/
disclosure.rs1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
4use baseid_core::error::CryptoError;
5use rand::Rng;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Disclosure {
12 pub salt: String,
14 pub claim_name: Option<String>,
16 pub claim_value: serde_json::Value,
18}
19
20impl Disclosure {
21 pub fn new(claim_name: Option<String>, value: serde_json::Value) -> Self {
23 let mut salt_bytes = [0u8; 16];
24 rand::thread_rng().fill(&mut salt_bytes);
25 let salt = URL_SAFE_NO_PAD.encode(salt_bytes);
26 Self {
27 salt,
28 claim_name,
29 claim_value: value,
30 }
31 }
32
33 pub fn encode(&self) -> baseid_core::Result<String> {
38 let array = match &self.claim_name {
39 Some(name) => serde_json::json!([self.salt, name, self.claim_value]),
40 None => serde_json::json!([self.salt, self.claim_value]),
41 };
42 let json =
43 serde_json::to_string(&array).map_err(baseid_core::error::SerializationError::Json)?;
44 Ok(URL_SAFE_NO_PAD.encode(json.as_bytes()))
45 }
46
47 pub fn decode(encoded: &str) -> baseid_core::Result<Self> {
49 let bytes = URL_SAFE_NO_PAD
50 .decode(encoded)
51 .map_err(|_| CryptoError::VerificationFailed)?;
52 let array: Vec<serde_json::Value> =
53 serde_json::from_slice(&bytes).map_err(baseid_core::error::SerializationError::Json)?;
54
55 match array.len() {
56 2 => {
58 let salt = array[0]
59 .as_str()
60 .ok_or(CryptoError::VerificationFailed)?
61 .to_string();
62 Ok(Self {
63 salt,
64 claim_name: None,
65 claim_value: array[1].clone(),
66 })
67 }
68 3 => {
70 let salt = array[0]
71 .as_str()
72 .ok_or(CryptoError::VerificationFailed)?
73 .to_string();
74 let name = array[1]
75 .as_str()
76 .ok_or(CryptoError::VerificationFailed)?
77 .to_string();
78 Ok(Self {
79 salt,
80 claim_name: Some(name),
81 claim_value: array[2].clone(),
82 })
83 }
84 _ => Err(CryptoError::VerificationFailed.into()),
85 }
86 }
87
88 pub fn digest(&self) -> baseid_core::Result<String> {
90 let encoded = self.encode()?;
91 let hash = Sha256::digest(encoded.as_bytes());
92 Ok(URL_SAFE_NO_PAD.encode(hash))
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn encode_decode_roundtrip_object() {
102 let d = Disclosure::new(Some("name".to_string()), serde_json::json!("Alice"));
103 let encoded = d.encode().unwrap();
104 let decoded = Disclosure::decode(&encoded).unwrap();
105 assert_eq!(decoded.salt, d.salt);
106 assert_eq!(decoded.claim_name, Some("name".to_string()));
107 assert_eq!(decoded.claim_value, serde_json::json!("Alice"));
108 }
109
110 #[test]
111 fn encode_decode_roundtrip_array() {
112 let d = Disclosure::new(None, serde_json::json!(42));
113 let encoded = d.encode().unwrap();
114 let decoded = Disclosure::decode(&encoded).unwrap();
115 assert_eq!(decoded.salt, d.salt);
116 assert_eq!(decoded.claim_name, None);
117 assert_eq!(decoded.claim_value, serde_json::json!(42));
118 }
119
120 #[test]
121 fn random_salt_uniqueness() {
122 let d1 = Disclosure::new(Some("a".to_string()), serde_json::json!(1));
123 let d2 = Disclosure::new(Some("a".to_string()), serde_json::json!(1));
124 assert_ne!(d1.salt, d2.salt);
125 }
126
127 #[test]
128 fn digest_determinism() {
129 let d = Disclosure {
130 salt: "fixed_salt".to_string(),
131 claim_name: Some("name".to_string()),
132 claim_value: serde_json::json!("Alice"),
133 };
134 let h1 = d.digest().unwrap();
135 let h2 = d.digest().unwrap();
136 assert_eq!(h1, h2);
137 }
138
139 fn verify_disclosure_vector(v: &baseid_test_vectors::sd_jwt::DisclosureVector) {
142 let d = Disclosure {
143 salt: v.salt.to_string(),
144 claim_name: v.claim_name.map(|s| s.to_string()),
145 claim_value: serde_json::from_str(v.claim_value).unwrap(),
146 };
147
148 let encoded = d.encode().unwrap();
150 assert_eq!(encoded, v.encoded, "encoding mismatch for salt={}", v.salt);
151
152 let digest = d.digest().unwrap();
154 assert_eq!(digest, v.digest, "digest mismatch for salt={}", v.salt);
155
156 let decoded = Disclosure::decode(&encoded).unwrap();
158 assert_eq!(decoded.salt, v.salt);
159 assert_eq!(decoded.claim_name.as_deref(), v.claim_name);
160 assert_eq!(decoded.claim_value, d.claim_value);
161 }
162
163 #[test]
164 fn vector_all_disclosures() {
165 for v in baseid_test_vectors::sd_jwt::ALL {
166 verify_disclosure_vector(v);
167 }
168 }
169
170 #[test]
171 fn vector_ietf_disclosure_decode() {
172 use baseid_test_vectors::sd_jwt;
173
174 let d = Disclosure::decode(sd_jwt::IETF_DISCLOSURE_WITH_SPACES).unwrap();
176 assert_eq!(d.salt, sd_jwt::IETF_DISCLOSURE_SALT);
177 assert_eq!(d.claim_name.as_deref(), Some(sd_jwt::IETF_DISCLOSURE_NAME));
178 assert_eq!(
179 d.claim_value,
180 serde_json::json!(sd_jwt::IETF_DISCLOSURE_VALUE)
181 );
182 }
183}