1#![cfg_attr(
26 feature = "dag-json",
27 doc = "
28 With the feature `dag-json` the JOSE values may also be encoded to DAG-JSON.
29
30 ```
31 use dag_jose::{DagJoseCodec, Jose};
32 use ipld_core::codec::Codec;
33 use serde_ipld_dagjson::codec::DagJsonCodec;
34
35 let data = hex::decode(\"
36 a2677061796c6f616458240171122089556551c3926679cc52c72e182a5619056a4727409ee93a26
37 d05ad727ca11f46a7369676e61747572657381a26970726f7465637465644f7b22616c67223a2245
38 64445341227d697369676e61747572655840fbff49e4e65c979955b9196023534913373416a11beb
39 fdb256c9146903ddb9c450e287be379ca70a5e7bc039b848fb66d4bd5b96dae986941e04e7968d55
40 b505\".chars().filter(|c| !c.is_whitespace()).collect::<String>()).unwrap();
41
42 // Decode binary data into an JOSE value.
43 let jose: Jose = DagJoseCodec::decode_from_slice(&data).unwrap();
44
45 // Encode an JOSE value into DAG-JSON bytes
46 let bytes = DagJsonCodec::encode_to_vec(&jose).unwrap();
47
48 assert_eq!(String::from_utf8(bytes).unwrap(), r#\"{
49 \"link\":{\"/\":\"bafyreiejkvsvdq4smz44yuwhfymcuvqzavveoj2at3utujwqlllspsqr6q\"},
50 \"payload\":\"AXESIIlVZVHDkmZ5zFLHLhgqVhkFakcnQJ7pOibQWtcnyhH0\",
51 \"signatures\":[{
52 \"protected\":\"eyJhbGciOiJFZERTQSJ9\",
53 \"signature\":\"-_9J5OZcl5lVuRlgI1NJEzc0FqEb6_2yVskUaQPducRQ4oe-N5ynCl57wDm4SPtm1L1bltrphpQeBOeWjVW1BQ\"
54 }]}\"#.chars().filter(|c| !c.is_whitespace()).collect::<String>());
55 ```
56 "
57)]
58#![cfg_attr(
59 not(feature = "dag-json"),
60 doc = "Enable the feature 'dag-json' to be able to encode/decode Jose values using DAG-JSON."
61)]
62#![deny(missing_docs)]
63
64mod bytes;
65mod codec;
66mod error;
67
68use std::collections::BTreeMap;
69
70use ipld_core::{
71 cid::Cid,
72 codec::{Codec, Links},
73 ipld,
74 ipld::Ipld,
75};
76use serde_derive::Serialize;
77use serde_ipld_dagcbor::codec::DagCborCodec;
78#[cfg(feature = "dag-json")]
79use serde_ipld_dagjson::codec::DagJsonCodec;
80
81use codec::Encoded;
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
85pub struct DagJoseCodec;
86
87impl Links for DagJoseCodec {
88 type LinksError = error::Error;
89
90 fn links(bytes: &[u8]) -> Result<impl Iterator<Item = Cid>, Self::LinksError> {
91 Ok(DagCborCodec::links(bytes)?)
92 }
93}
94impl Codec<Ipld> for DagJoseCodec {
95 const CODE: u64 = 0x85;
96
97 type Error = error::Error;
98
99 fn decode<R: std::io::BufRead>(reader: R) -> Result<Ipld, Self::Error> {
100 Ok(serde_ipld_dagcbor::from_reader(reader)?)
101 }
102
103 fn encode<W: std::io::Write>(writer: W, data: &Ipld) -> Result<(), Self::Error> {
104 Ok(serde_ipld_dagcbor::to_writer(writer, data)?)
105 }
106}
107
108#[derive(Clone, Debug, PartialEq)]
110pub enum Jose {
111 Signature(JsonWebSignature),
113 Encryption(JsonWebEncryption),
115}
116
117impl Codec<Jose> for DagJoseCodec {
118 const CODE: u64 = 0x85;
119
120 type Error = error::Error;
121
122 fn decode<R: std::io::BufRead>(reader: R) -> Result<Jose, Self::Error> {
123 let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?;
124 encoded.try_into()
125 }
126
127 fn encode<W: std::io::Write>(writer: W, data: &Jose) -> Result<(), Self::Error> {
128 let encoded: Encoded = data.try_into()?;
129 Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?)
130 }
131}
132
133#[cfg(feature = "dag-json")]
134impl Codec<Jose> for DagJsonCodec {
135 const CODE: u64 = 0x0129;
136 type Error = error::Error;
137
138 fn decode<R: std::io::BufRead>(reader: R) -> Result<Jose, Self::Error> {
139 let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?;
140 encoded.try_into()
141 }
142
143 fn encode<W: std::io::Write>(writer: W, data: &Jose) -> Result<(), Self::Error> {
144 match data {
145 Jose::Signature(jws) => DagJsonCodec::encode(writer, jws),
146 Jose::Encryption(jwe) => DagJsonCodec::encode(writer, jwe),
147 }
148 }
149}
150
151#[derive(Clone, Debug, PartialEq, Serialize)]
153pub struct JsonWebSignature {
154 pub link: Cid,
156
157 pub payload: String,
159
160 pub signatures: Vec<Signature>,
162}
163
164impl<'a> From<&'a JsonWebSignature> for Ipld {
165 fn from(value: &'a JsonWebSignature) -> Self {
166 ipld!({
167 "payload": value.payload.to_owned(),
168 "signatures": value.signatures.iter().map(Ipld::from).collect::<Vec<Ipld>>(),
169 "link": value.link,
170 })
171 }
172}
173
174impl Codec<JsonWebSignature> for DagJoseCodec {
175 const CODE: u64 = 0x85;
176
177 type Error = error::Error;
178
179 fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebSignature, Self::Error> {
180 let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?;
181 encoded.try_into()
182 }
183
184 fn encode<W: std::io::Write>(writer: W, data: &JsonWebSignature) -> Result<(), Self::Error> {
185 let encoded: Encoded = data.try_into()?;
186 Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?)
187 }
188}
189
190#[cfg(feature = "dag-json")]
191impl Codec<JsonWebSignature> for DagJsonCodec {
192 const CODE: u64 = 0x0129;
193
194 type Error = error::Error;
195
196 fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebSignature, Self::Error> {
197 let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?;
198 encoded.try_into()
199 }
200
201 fn encode<W: std::io::Write>(writer: W, data: &JsonWebSignature) -> Result<(), Self::Error> {
202 Ok(serde_ipld_dagjson::to_writer(writer, &data)?)
206 }
207}
208
209#[derive(Clone, Debug, PartialEq, Serialize)]
211pub struct Signature {
212 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
214 pub header: BTreeMap<String, Ipld>,
215 pub protected: Option<String>,
217 pub signature: String,
219}
220
221impl<'a> From<&'a Signature> for Ipld {
222 fn from(value: &'a Signature) -> Self {
223 let mut fields: BTreeMap<String, Ipld> = BTreeMap::new();
224 if !value.header.is_empty() {
225 fields.insert("header".to_string(), value.header.to_owned().into());
226 }
227 if let Some(protected) = value.protected.to_owned() {
228 fields.insert("protected".to_string(), protected.into());
229 };
230 fields.insert("signature".to_string(), value.signature.to_owned().into());
231 Ipld::Map(fields)
232 }
233}
234
235#[derive(Clone, Debug, PartialEq, Serialize)]
237pub struct JsonWebEncryption {
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub aad: Option<String>,
241
242 pub ciphertext: String,
245
246 pub iv: String,
248
249 pub protected: String,
251
252 #[serde(skip_serializing_if = "Vec::is_empty")]
254 pub recipients: Vec<Recipient>,
255
256 pub tag: String,
258
259 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
261 pub unprotected: BTreeMap<String, Ipld>,
262}
263
264impl<'a> From<&'a JsonWebEncryption> for Ipld {
265 fn from(value: &'a JsonWebEncryption) -> Self {
266 let mut fields: BTreeMap<String, Ipld> = BTreeMap::new();
267 if let Some(aad) = value.aad.to_owned() {
268 fields.insert("aad".to_string(), aad.into());
269 }
270 fields.insert("ciphertext".to_string(), value.ciphertext.to_owned().into());
271 fields.insert("iv".to_string(), value.iv.to_owned().into());
272 fields.insert("protected".to_string(), value.protected.to_owned().into());
273 if !value.recipients.is_empty() {
274 fields.insert(
275 "recipients".to_string(),
276 value
277 .recipients
278 .iter()
279 .map(Ipld::from)
280 .collect::<Vec<Ipld>>()
281 .into(),
282 );
283 }
284
285 fields.insert("tag".to_string(), value.tag.to_owned().into());
286 if !value.unprotected.is_empty() {
287 fields.insert(
288 "unprotected".to_string(),
289 value.unprotected.to_owned().into(),
290 );
291 }
292 Ipld::Map(fields)
293 }
294}
295
296impl Codec<JsonWebEncryption> for DagJoseCodec {
297 const CODE: u64 = 0x85;
298
299 type Error = error::Error;
300
301 fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebEncryption, Self::Error> {
302 let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?;
303 encoded.try_into()
304 }
305
306 fn encode<W: std::io::Write>(writer: W, data: &JsonWebEncryption) -> Result<(), Self::Error> {
307 let encoded: Encoded = data.try_into()?;
308 Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?)
309 }
310}
311
312#[cfg(feature = "dag-json")]
313impl Codec<JsonWebEncryption> for DagJsonCodec {
314 const CODE: u64 = 0x0129;
315
316 type Error = error::Error;
317
318 fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebEncryption, Self::Error> {
319 let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?;
320 encoded.try_into()
321 }
322
323 fn encode<W: std::io::Write>(writer: W, data: &JsonWebEncryption) -> Result<(), Self::Error> {
324 Ok(serde_ipld_dagjson::to_writer(writer, &data)?)
328 }
329}
330
331#[derive(Clone, Debug, PartialEq, Serialize)]
333pub struct Recipient {
334 pub encrypted_key: Option<String>,
336
337 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
339 pub header: BTreeMap<String, Ipld>,
340}
341
342impl<'a> From<&'a Recipient> for Ipld {
343 fn from(value: &'a Recipient) -> Self {
344 let mut fields: BTreeMap<String, Ipld> = BTreeMap::new();
345 if let Some(encrypted_key) = value.encrypted_key.to_owned() {
346 fields.insert("encrypted_key".to_string(), encrypted_key.into());
347 }
348 if !value.header.is_empty() {
349 fields.insert("header".to_string(), value.header.to_owned().into());
350 }
351 Ipld::Map(fields)
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use std::collections::BTreeMap;
358
359 use super::*;
360
361 struct JwsFixture {
362 payload: Box<[u8]>,
363 protected: Box<[u8]>,
364 signature: Box<[u8]>,
365 }
366 fn fixture_jws() -> JwsFixture {
367 let payload =
368 base64_url::decode("AXESIIlVZVHDkmZ5zFLHLhgqVhkFakcnQJ7pOibQWtcnyhH0").unwrap();
369 let protected = base64_url::decode("eyJhbGciOiJFZERTQSJ9").unwrap();
370 let signature = base64_url::decode("-_9J5OZcl5lVuRlgI1NJEzc0FqEb6_2yVskUaQPducRQ4oe-N5ynCl57wDm4SPtm1L1bltrphpQeBOeWjVW1BQ").unwrap();
371 JwsFixture {
372 payload: payload.into_boxed_slice(),
373 protected: protected.into_boxed_slice(),
374 signature: signature.into_boxed_slice(),
375 }
376 }
377 fn fixture_jws_base64(
378 payload: &[u8],
379 protected: &[u8],
380 signature: &[u8],
381 ) -> (String, String, String) {
382 (
383 base64_url::encode(payload),
384 base64_url::encode(protected),
385 base64_url::encode(signature),
386 )
387 }
388 struct JweFixture {
389 ciphertext: Box<[u8]>,
390 iv: Box<[u8]>,
391 protected: Box<[u8]>,
392 tag: Box<[u8]>,
393 }
394 fn fixture_jwe() -> JweFixture {
395 let ciphertext = base64_url::decode("3XqLW28NHP-raqW8vMfIHOzko4N3IRaR").unwrap();
396 let iv = base64_url::decode("PSWIuAyO8CpevzCL").unwrap();
397 let protected = base64_url::decode("eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0").unwrap();
398 let tag = base64_url::decode("WZAMBblhzDCsQWOAKdlkSA").unwrap();
399 JweFixture {
400 ciphertext: ciphertext.into_boxed_slice(),
401 iv: iv.into_boxed_slice(),
402 protected: protected.into_boxed_slice(),
403 tag: tag.into_boxed_slice(),
404 }
405 }
406 fn fixture_jwe_base64(
407 ciphertext: &[u8],
408 iv: &[u8],
409 protected: &[u8],
410 tag: &[u8],
411 ) -> (String, String, String, String) {
412 (
413 base64_url::encode(ciphertext),
414 base64_url::encode(iv),
415 base64_url::encode(protected),
416 base64_url::encode(tag),
417 )
418 }
419 #[test]
420 fn roundtrip_jws() {
421 let JwsFixture {
422 payload,
423 protected,
424 signature,
425 } = fixture_jws();
426 let (payload_b64, protected_b64, signature_b64) =
427 fixture_jws_base64(&payload, &protected, &signature);
428 let link = Cid::try_from(base64_url::decode(&payload_b64).unwrap()).unwrap();
429 assert_roundtrip(
430 DagJoseCodec,
431 &JsonWebSignature {
432 payload: payload_b64,
433 signatures: vec![Signature {
434 header: BTreeMap::from([
435 ("k0".to_string(), Ipld::from("v0")),
436 ("k1".to_string(), Ipld::from(1)),
437 ]),
438 protected: Some(protected_b64),
439 signature: signature_b64,
440 }],
441 link,
442 },
443 &ipld!({
444 "payload": payload,
445 "signatures": [{
446 "header": {
447 "k0": "v0",
448 "k1": 1
449 },
450 "protected": protected,
451 "signature": signature,
452 }],
453 }),
454 );
455 }
456 #[test]
457 fn roundtrip_jwe() {
458 let JweFixture {
459 ciphertext,
460 iv,
461 protected,
462 tag,
463 } = fixture_jwe();
464 let (ciphertext_b64, iv_b64, protected_b64, tag_b64) =
465 fixture_jwe_base64(&ciphertext, &iv, &protected, &tag);
466 assert_roundtrip(
467 DagJoseCodec,
468 &JsonWebEncryption {
469 aad: None,
470 ciphertext: ciphertext_b64,
471 iv: iv_b64,
472 protected: protected_b64,
473 recipients: vec![],
474 tag: tag_b64,
475 unprotected: BTreeMap::new(),
476 },
477 &ipld!({
478 "ciphertext": ciphertext,
479 "iv": iv,
480 "protected": protected,
481 "tag": tag,
482 }),
483 );
484 }
485
486 fn assert_roundtrip<C, T>(_c: C, data: &T, ipld: &Ipld)
490 where
491 C: Codec<T>,
492 C: Codec<Ipld>,
493 <C as Codec<T>>::Error: std::fmt::Debug,
494 <C as Codec<Ipld>>::Error: std::fmt::Debug,
495 T: std::cmp::PartialEq + std::fmt::Debug,
496 {
497 let bytes = C::encode_to_vec(data).unwrap();
498 let bytes2 = C::encode_to_vec(ipld).unwrap();
499 if bytes != bytes2 {
500 panic!(
501 r#"assertion failed: `(left == right)`
502 left: `{}`,
503 right: `{}`"#,
504 hex::encode(&bytes),
505 hex::encode(&bytes2)
506 );
507 }
508 let ipld2: Ipld = C::decode_from_slice(&bytes).unwrap();
509 assert_eq!(&ipld2, ipld);
510 let data2: T = C::decode_from_slice(&bytes).unwrap();
511 assert_eq!(&data2, data);
512 }
513}