1use core::fmt;
4
5use serde_json::{Map, Value};
6
7use super::sd_jwt::{ELLIPSIS_KEY, SD_KEY};
8use super::{ClaimPath, Disclosure, HashAlgorithm, SdJwt, SdJwtError};
9use crate::jose::{JwsHeader, Signer, base64url, encode_compact};
10
11pub struct SdJwtBuilder {
53 claims: Map<String, Value>,
54 concealed: Vec<ClaimPath>,
55 decoys: usize,
56 alg: HashAlgorithm,
57 salts: Box<dyn FnMut() -> Result<String, SdJwtError> + Send>,
58}
59
60impl SdJwtBuilder {
61 pub const MAX_DEPTH: usize = 64;
70
71 pub fn new(claims: Map<String, Value>) -> Self {
73 Self {
74 claims,
75 concealed: Vec::new(),
76 decoys: 0,
77 alg: HashAlgorithm::default(),
78 salts: Box::new(random_salt),
79 }
80 }
81
82 #[must_use]
85 pub fn conceal(mut self, path: impl Into<ClaimPath>) -> Self {
86 self.concealed.push(path.into());
87 self
88 }
89
90 #[must_use]
92 pub fn with_decoys(mut self, count: usize) -> Self {
93 self.decoys = count;
94 self
95 }
96
97 #[must_use]
99 pub fn with_hash_algorithm(mut self, alg: HashAlgorithm) -> Self {
100 self.alg = alg;
101 self
102 }
103
104 #[must_use]
108 pub fn with_salt_generator(
109 mut self,
110 mut salts: impl FnMut() -> String + Send + 'static,
111 ) -> Self {
112 self.salts = Box::new(move || Ok(salts()));
113 self
114 }
115
116 pub fn claims(&self) -> &Map<String, Value> {
118 &self.claims
119 }
120
121 pub fn concealed(&self) -> &[ClaimPath] {
123 &self.concealed
124 }
125
126 pub fn encode(mut self) -> Result<Encoded, SdJwtError> {
136 if self.claims.contains_key(HashAlgorithm::CLAIM) {
137 return Err(SdJwtError::payload("the claims already carry `_sd_alg`"));
138 }
139 if self
140 .claims
141 .values()
142 .any(|value| nests_deeper_than(value, Self::MAX_DEPTH - 1))
143 {
144 return Err(SdJwtError::TooDeep {
145 max: Self::MAX_DEPTH,
146 });
147 }
148 let mut matched = vec![false; self.concealed.len()];
149 let mut disclosures = Vec::new();
150 let mut payload = self.object(
151 self.claims.clone(),
152 &ClaimPath::root(),
153 &mut matched,
154 &mut disclosures,
155 )?;
156 if let Some(unmatched) = matched.iter().position(|m| !m) {
157 return Err(SdJwtError::NoSuchClaim(self.concealed[unmatched].clone()));
158 }
159 if !disclosures.is_empty() || payload.contains_key(SD_KEY) {
160 payload.insert(
161 HashAlgorithm::CLAIM.to_owned(),
162 Value::String(self.alg.name().to_owned()),
163 );
164 }
165 Ok(Encoded {
166 payload,
167 disclosures,
168 })
169 }
170
171 pub async fn sign(self, header: &JwsHeader, signer: &dyn Signer) -> Result<SdJwt, SdJwtError> {
181 let encoded = self.encode()?;
182 let payload = serde_json::to_vec(&encoded.payload).expect("a JSON object serializes");
183 let jwt = encode_compact(header, &payload, signer).await?;
184 Ok(SdJwt::new(
185 crate::vc::Jwt::new(jwt).expect("a freshly encoded JWS decodes"),
186 encoded.disclosures,
187 ))
188 }
189
190 fn is_concealed(&self, path: &ClaimPath, matched: &mut [bool]) -> bool {
192 let mut concealed = false;
193 for (i, pattern) in self.concealed.iter().enumerate() {
194 if pattern.matches(path) {
195 matched[i] = true;
196 concealed = true;
197 }
198 }
199 concealed
200 }
201
202 fn value(
203 &mut self,
204 value: Value,
205 path: &ClaimPath,
206 matched: &mut [bool],
207 disclosures: &mut Vec<Disclosure>,
208 ) -> Result<Value, SdJwtError> {
209 match value {
210 Value::Object(object) => self
211 .object(object, path, matched, disclosures)
212 .map(Value::Object),
213 Value::Array(array) => self
214 .array(array, path, matched, disclosures)
215 .map(Value::Array),
216 other => Ok(other),
217 }
218 }
219
220 fn object(
221 &mut self,
222 object: Map<String, Value>,
223 path: &ClaimPath,
224 matched: &mut [bool],
225 disclosures: &mut Vec<Disclosure>,
226 ) -> Result<Map<String, Value>, SdJwtError> {
227 if object.contains_key(SD_KEY) {
228 return Err(SdJwtError::payload(format!(
229 "the object at {path} already carries `_sd`"
230 )));
231 }
232 let mut out = Map::new();
233 let mut digests = Vec::new();
234 for (name, value) in object {
235 let child = path.join(name.as_str());
236 let value = self.value(value, &child, matched, disclosures)?;
237 if !self.is_concealed(&child, matched) {
238 out.insert(name, value);
239 continue;
240 }
241 if name == SD_KEY || name == ELLIPSIS_KEY {
242 return Err(SdJwtError::NotConcealable {
243 path: child,
244 reason: format!("`{name}` is reserved"),
245 });
246 }
247 let disclosure = Disclosure::property((self.salts)()?, name, value);
248 digests.push(disclosure.digest(self.alg));
249 disclosures.push(disclosure);
250 }
251 if !digests.is_empty() {
252 for _ in 0..self.decoys {
253 digests.push(self.alg.digest((self.salts)()?.as_bytes()));
254 }
255 digests.sort_unstable();
256 out.insert(
257 SD_KEY.to_owned(),
258 Value::Array(digests.into_iter().map(Value::String).collect()),
259 );
260 }
261 Ok(out)
262 }
263
264 fn array(
265 &mut self,
266 array: Vec<Value>,
267 path: &ClaimPath,
268 matched: &mut [bool],
269 disclosures: &mut Vec<Disclosure>,
270 ) -> Result<Vec<Value>, SdJwtError> {
271 let mut out = Vec::with_capacity(array.len());
272 for (i, element) in array.into_iter().enumerate() {
273 let child = path.join(i);
274 let element = self.value(element, &child, matched, disclosures)?;
275 if !self.is_concealed(&child, matched) {
276 out.push(element);
277 continue;
278 }
279 let disclosure = Disclosure::element((self.salts)()?, element);
280 let mut placeholder = Map::new();
281 placeholder.insert(
282 ELLIPSIS_KEY.to_owned(),
283 Value::String(disclosure.digest(self.alg)),
284 );
285 disclosures.push(disclosure);
286 out.push(Value::Object(placeholder));
287 }
288 Ok(out)
289 }
290}
291
292impl fmt::Debug for SdJwtBuilder {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 f.debug_struct("SdJwtBuilder")
295 .field("claims", &self.claims)
296 .field("concealed", &self.concealed)
297 .field("decoys", &self.decoys)
298 .field("alg", &self.alg)
299 .finish_non_exhaustive()
300 }
301}
302
303fn random_salt() -> Result<String, SdJwtError> {
305 let mut bytes = [0u8; 16];
306 getrandom::fill(&mut bytes).map_err(|e| SdJwtError::Randomness(Box::new(e)))?;
307 Ok(base64url::encode(&bytes))
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
312#[non_exhaustive]
313pub struct Encoded {
314 pub payload: Map<String, Value>,
317 pub disclosures: Vec<Disclosure>,
319}
320
321fn nests_deeper_than(value: &Value, limit: usize) -> bool {
325 match value {
326 Value::Array(items) => {
327 limit == 0 || items.iter().any(|item| nests_deeper_than(item, limit - 1))
328 }
329 Value::Object(members) => {
330 limit == 0
331 || members
332 .values()
333 .any(|member| nests_deeper_than(member, limit - 1))
334 }
335 _ => false,
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use std::error::Error as _;
342
343 use serde_json::json;
344
345 use super::*;
346
347 fn without_randomness(claims: &Value) -> SdJwtBuilder {
349 let mut builder = SdJwtBuilder::new(claims.as_object().unwrap().clone());
350 builder.salts = Box::new(|| {
351 Err(SdJwtError::Randomness(Box::new(std::io::Error::other(
352 "no entropy",
353 ))))
354 });
355 builder
356 }
357
358 #[test]
359 fn a_failing_random_number_generator_is_an_error() {
360 let claims = json!({"given_name": "Erika", "nationalities": ["DE"]});
361 for builder in [
362 without_randomness(&claims).conceal(["given_name"]),
363 without_randomness(&claims).conceal(crate::claim_path!["nationalities", 0]),
364 ] {
365 let error = builder.encode().unwrap_err();
366 assert!(matches!(error, SdJwtError::Randomness(_)), "{error:?}");
367 assert_eq!(error.to_string(), "cannot generate a salt: no entropy");
368 assert!(error.source().unwrap().is::<std::io::Error>());
369 }
370 let encoded = without_randomness(&claims).with_decoys(2).encode().unwrap();
372 assert_eq!(Value::Object(encoded.payload), claims);
373 }
374
375 #[test]
376 fn decoys_draw_salts_too() {
377 let mut calls = 0;
378 let mut builder = SdJwtBuilder::new(json!({"a": 1}).as_object().unwrap().clone())
379 .conceal(["a"])
380 .with_decoys(1);
381 builder.salts = Box::new(move || {
382 calls += 1;
383 if calls == 1 {
384 Ok("salt".to_owned())
385 } else {
386 Err(SdJwtError::Randomness(Box::new(std::io::Error::other(
387 "no entropy",
388 ))))
389 }
390 });
391 assert!(matches!(builder.encode(), Err(SdJwtError::Randomness(_))));
392 }
393}