json_digest_wasm/
lib.rs

1#![warn(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3
4//! This library provides some algorithms to calculate cryptographically secure digests of JSON documents.
5//! Since JSON is an ambiguous serialization format, we also had to define a canonical deterministic subset
6//! of all allowed documents. Order of keys in an object and Unicode normalization are well-defined in this
7//! subset, making it suitable for hashing.
8
9use wasm_bindgen::prelude::*;
10
11use json_digest::*;
12
13/// Converts any error that can be converted into a string into a JavaScript error string
14/// usable by wasm_bindgen
15pub fn err_to_js<E: ToString>(e: E) -> JsValue {
16    JsValue::from(e.to_string())
17}
18
19/// An extension trait on [`Result`] that helps easy conversion of Rust errors to JavaScript
20/// error strings usable by wasm_bindgen
21pub trait MapJsError<T> {
22    /// An extension method on [`Result`] to easily convert Rust errors to JavaScript ones.
23    ///
24    /// ```ignore
25    /// #[wasm_bindgen]
26    /// pub fn method(&self) -> Result<JsSomething, JsValue> {
27    ///     let result: JsSomething = self.fallible().map_err_to_js()?;
28    ///     Ok(result)
29    /// }
30    /// ```
31    fn map_err_to_js(self) -> Result<T, JsValue>;
32}
33
34impl<T, E: ToString> MapJsError<T> for Result<T, E> {
35    fn map_err_to_js(self) -> Result<T, JsValue> {
36        self.map_err(err_to_js)
37    }
38}
39
40/// Returns a canonical string representation of a JSON document, in which any sub-objects not explicitly listed in the
41/// second argument are collapsed to their digest. The format of the second argument is inspired by
42/// [JQ basic filters](https://stedolan.github.io/jq/manual/#Basicfilters) and these are some examples:
43///
44/// ```json
45/// {
46///     "a": {
47///         "1": "apple",
48///         "2": "banana"
49///     },
50///     "b": ["some", "array", 0xf, "values"],
51///     "c": 42
52/// }
53/// ```
54///
55/// - "" -> Same as calling {@link digestJson}
56/// - ".a" -> Keep property "a" untouched, the rest will be replaced with their digest. Note that most likely the scalar number "c"
57///   does not have enough entropy to avoid a brute-force attack for its digest.
58/// - ".b, .c" -> Keeps both properties "b" and "c" unaltered, but "a" will be replaced with the digest of that sub-object.
59///
60/// You should protect scalar values and easy-to-guess lists by replacing them with an object that has an extra "nonce" property, which
61/// has enough entropy. @see wrapJsonWithNonce
62#[wasm_bindgen(js_name = selectiveDigestJson)]
63pub fn selective_digest(data: &JsValue, keep_properties_list: &str) -> Result<String, JsValue> {
64    let serde_data: serde_json::Value = data.into_serde().map_err_to_js()?;
65    let digested_data_str =
66        selective_digest_json(&serde_data, keep_properties_list).map_err_to_js()?;
67    Ok(digested_data_str)
68}
69
70/// Calculates the digest of a JSON document. Since this digest is calculated by recursively replacing sub-objects with their digest,
71/// it is possible to selectively reveal parts of the document using {@link selectiveDigestJson}
72#[wasm_bindgen(js_name = digestJson)]
73pub fn digest(data: &JsValue) -> Result<String, JsValue> {
74    selective_digest(data, "")
75}
76
77/// This function provides a canonical string for any JSON document. Order of the keys in objects, whitespace
78/// and unicode normalization are all taken care of, so document that belongs to a single digest is not malleable.
79///
80/// This is a drop-in replacement for `JSON.stringify(data)`
81#[wasm_bindgen(js_name = stringifyJson)]
82pub fn stringify(data: &JsValue) -> Result<String, JsValue> {
83    let serde_data: serde_json::Value = data.into_serde().map_err_to_js()?;
84    let stringified = canonical_json(&serde_data).map_err_to_js()?;
85    Ok(stringified)
86}
87
88/// You should protect scalar values and easy-to-guess lists by replacing them with an object that has an extra "nonce" property, which
89/// has enough entropy. List of all countries, cities in a country, streets in a city are all easy to enumerate for a brute-fore
90/// attack.
91///
92/// For example if you have a string that is a country, you can call this function like `wrapJsonWithNonce("Germany")` and get an
93/// object like the following:
94///
95/// ```json
96/// {
97///     "nonce": "ukhFsI4a6vIZEDUOBRxJmLroPEQ8FQCjJwbI-Z7bEocGo",
98///     "value": "Germany"
99/// }
100/// ```
101#[wasm_bindgen(js_name = wrapWithNonce)]
102pub fn wrap_with_nonce(data: &JsValue) -> Result<JsValue, JsValue> {
103    let serde_data: serde_json::Value = data.into_serde().map_err_to_js()?;
104    let nonce = Nonce264::generate();
105    let wrapped_serde = serde_json::json!({
106        "nonce": nonce.0,
107        "value": serde_data,
108    });
109    let wrapped_json = JsValue::from_serde(&wrapped_serde).map_err_to_js()?;
110    Ok(wrapped_json)
111}