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}