Elliptic Curve Multiset Hash (ECMH) for Rust and WebAssembly
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feature: wasm and usage documentation

+615 -6
+69
Cargo.lock
··· 24 24 ] 25 25 26 26 [[package]] 27 + name = "bumpalo" 28 + version = "3.20.2" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 31 + 32 + [[package]] 27 33 name = "cfg-if" 28 34 version = "1.0.4" 29 35 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 137 143 "curve25519-dalek", 138 144 "elliptic-curve", 139 145 "hex", 146 + "js-sys", 140 147 "k256", 141 148 "p256", 142 149 "serde", 143 150 "sha2", 151 + "wasm-bindgen", 144 152 ] 145 153 146 154 [[package]] ··· 225 233 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 226 234 dependencies = [ 227 235 "digest", 236 + ] 237 + 238 + [[package]] 239 + name = "js-sys" 240 + version = "0.3.94" 241 + source = "registry+https://github.com/rust-lang/crates.io-index" 242 + checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" 243 + dependencies = [ 244 + "once_cell", 245 + "wasm-bindgen", 228 246 ] 229 247 230 248 [[package]] ··· 340 358 ] 341 359 342 360 [[package]] 361 + name = "rustversion" 362 + version = "1.0.22" 363 + source = "registry+https://github.com/rust-lang/crates.io-index" 364 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 365 + 366 + [[package]] 343 367 name = "sec1" 344 368 version = "0.7.3" 345 369 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 460 484 version = "0.11.1+wasi-snapshot-preview1" 461 485 source = "registry+https://github.com/rust-lang/crates.io-index" 462 486 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 487 + 488 + [[package]] 489 + name = "wasm-bindgen" 490 + version = "0.2.117" 491 + source = "registry+https://github.com/rust-lang/crates.io-index" 492 + checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" 493 + dependencies = [ 494 + "cfg-if", 495 + "once_cell", 496 + "rustversion", 497 + "wasm-bindgen-macro", 498 + "wasm-bindgen-shared", 499 + ] 500 + 501 + [[package]] 502 + name = "wasm-bindgen-macro" 503 + version = "0.2.117" 504 + source = "registry+https://github.com/rust-lang/crates.io-index" 505 + checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" 506 + dependencies = [ 507 + "quote", 508 + "wasm-bindgen-macro-support", 509 + ] 510 + 511 + [[package]] 512 + name = "wasm-bindgen-macro-support" 513 + version = "0.2.117" 514 + source = "registry+https://github.com/rust-lang/crates.io-index" 515 + checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" 516 + dependencies = [ 517 + "bumpalo", 518 + "proc-macro2", 519 + "quote", 520 + "syn", 521 + "wasm-bindgen-shared", 522 + ] 523 + 524 + [[package]] 525 + name = "wasm-bindgen-shared" 526 + version = "0.2.117" 527 + source = "registry+https://github.com/rust-lang/crates.io-index" 528 + checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" 529 + dependencies = [ 530 + "unicode-ident", 531 + ] 463 532 464 533 [[package]] 465 534 name = "zeroize"
+7 -3
Cargo.toml
··· 5 5 description = "Elliptic Curve Multiset Hash - a homomorphic hash function for multisets" 6 6 license = "MIT OR Apache-2.0" 7 7 8 + [lib] 9 + crate-type = ["cdylib", "rlib"] 10 + 8 11 [dependencies] 9 12 curve25519-dalek = { version = "4", features = ["digest"], optional = true } 10 13 sha2 = "0.10" ··· 12 15 k256 = { version = "0.13", features = ["hash2curve", "arithmetic"], optional = true } 13 16 elliptic-curve = { version = "0.13", features = ["hash2curve", "sec1"], optional = true } 14 17 serde = { version = "1", features = ["derive"], optional = true } 15 - 16 - [dependencies.hex] 17 - version = "0.4" 18 + hex = "0.4" 19 + wasm-bindgen = { version = "0.2", optional = true } 20 + js-sys = { version = "0.3", optional = true } 18 21 19 22 [dev-dependencies] 20 23 ··· 24 27 p256 = ["dep:p256", "dep:elliptic-curve"] 25 28 k256 = ["dep:k256", "dep:elliptic-curve"] 26 29 serde = ["dep:serde"] 30 + wasm = ["dep:wasm-bindgen", "dep:js-sys", "ristretto"]
+63 -2
README.md
··· 1 1 # ecmh 2 2 3 - Elliptic Curve Multiset Hash (ECMH) for Rust — a homomorphic hash function for multisets based on [Maitin-Shepard et al. (2016)](https://arxiv.org/abs/1601.06502). 3 + Elliptic Curve Multiset Hash (ECMH) for Rust and WebAssembly — a homomorphic hash function for multisets based on [Maitin-Shepard et al. (2016)](https://arxiv.org/abs/1601.06502). 4 4 5 5 ECMH maps each set element to an elliptic curve point and sums them. The hash of the union of two multisets is simply the sum of their individual hashes, enabling efficient incremental updates without rehashing the entire set. 6 6 ··· 19 19 20 20 - **Operator overloading**: `+`, `-`, `+=`, `-=` for combining hashes 21 21 - **Custom curves**: implement the `CurveBackend` trait 22 + - **WebAssembly**: first-class wasm support via `wasm-pack` 22 23 23 24 ## Usage 25 + 26 + ### Rust 24 27 25 28 ```toml 26 29 [dependencies] ··· 34 37 ecmh = { version = "0.1", default-features = false, features = ["p256"] } 35 38 ``` 36 39 37 - ## Example 40 + ### WebAssembly 41 + 42 + Build the wasm package with `wasm-pack`: 43 + 44 + ```sh 45 + wasm-pack build --target web --features wasm 46 + ``` 47 + 48 + This produces a `pkg/` directory containing the `.wasm` binary, JS glue, and TypeScript definitions ready for `npm publish` or direct use. 49 + 50 + For additional curve backends in the wasm build: 51 + 52 + ```sh 53 + wasm-pack build --target web --features wasm,p256,k256 54 + ``` 55 + 56 + ## Example (Rust) 38 57 39 58 ```rust 40 59 use ecmh::RistrettoEcmh; ··· 72 91 let recovered = RistrettoEcmh::from_bytes(&bytes).unwrap(); 73 92 assert_eq!(recovered, hash); 74 93 ``` 94 + 95 + ## Example (JavaScript / TypeScript) 96 + 97 + ```typescript 98 + import init, { EcmhRistretto } from './pkg/ecmh.js'; 99 + 100 + await init(); 101 + 102 + const enc = new TextEncoder(); 103 + 104 + // Build a multiset hash incrementally 105 + const hash = new EcmhRistretto(); 106 + hash.insert(enc.encode("alice")); 107 + hash.insert(enc.encode("bob")); 108 + 109 + // Order doesn't matter 110 + const hash2 = new EcmhRistretto(); 111 + hash2.insert(enc.encode("bob")); 112 + hash2.insert(enc.encode("alice")); 113 + console.log(hash.equals(hash2)); // true 114 + 115 + // Combine disjoint sets 116 + const a = EcmhRistretto.fromElement(enc.encode("alice")); 117 + const b = EcmhRistretto.fromElement(enc.encode("bob")); 118 + console.log(a.union(b).equals(hash)); // true 119 + 120 + // Serialize 121 + console.log(hash.toHex()); // 64-char hex string 122 + console.log(hash.toBytes()); // Uint8Array(32) 123 + 124 + // Deserialize 125 + const recovered = EcmhRistretto.fromBytes(hash.toBytes()); 126 + console.log(recovered.equals(hash)); // true 127 + ``` 128 + 129 + The wasm build exposes one class per enabled curve backend: 130 + 131 + | JS class | Curve | Feature flags | 132 + |---|---|---| 133 + | `EcmhRistretto` | Ristretto255 | `wasm` | 134 + | `EcmhP256` | NIST P-256 | `wasm` + `p256` | 135 + | `EcmhK256` | secp256k1 | `wasm` + `k256` | 75 136 76 137 ## Custom curve backend 77 138
+194
USAGE.md
··· 1 + # Permissioned Repo Commits with ECMH 2 + 3 + This document describes how ECMH is used to build authenticated, per-reader commits for permissioned repositories. Each commit characterizes the current set of live records in a repo without exposing that information to other readers. 4 + 5 + ## Permissioned repo commits 6 + 7 + Each user's permissioned repo within a space is represented by a cryptographic commit: a compact digest that characterizes the current set of live records, independent of history. 8 + 9 + We use ECMH (Elliptic Curve Multiset Hash), a set hash where adding or removing an element is a single point operation rather than a full recompute. Two permissioned repos with the same live records produce the same ECMH digest regardless of the order records were written or deleted. 10 + 11 + This commitment plays the same role as the MST root hash for public data: a single value that definitively characterizes what is in the repo. Unlike the MST, it does not support partial sync or single-record proofs. The tradeoff is a noticeably lower-overhead cryptographic structure and sync protocol. 12 + 13 + ## The problem with bare ECMH digests 14 + 15 + ECMH is deterministic. If client-a and client-b happen to have permissioned repos with identical live records, their bare ECMH digests will be identical. A party who obtains both commits can tell that the two repos contain the same data, even without knowing what that data is. 16 + 17 + Worse, a bare digest can be rebroadcast. If client-a receives a commit from the server, nothing stops client-a from forwarding that commit to a third party who can then verify it against data they obtain independently. 18 + 19 + Both problems are solved by authenticating the ECMH digest with a randomly generated, transient HMAC key that is unique to each reader. 20 + 21 + ## Commit structure 22 + 23 + A permissioned repo commit is composed of four fields: 24 + 25 + ``` 26 + commit = { 27 + ecmh: bytes, // the ECMH digest of the live record set 28 + hmac: bytes, // HMAC-SHA256(hmac_key, ecmh) 29 + hmac_key: bytes, // randomly generated, unique per reader 30 + sig: bytes, // atproto signing key signature over (ecmh, hmac, hmac_key) 31 + } 32 + ``` 33 + 34 + The server generates a fresh HMAC key for every reader on every read. The signature binds all fields together under the repo owner's signing key. 35 + 36 + ## Why this works 37 + 38 + ### Different commits for identical repos 39 + 40 + Even when two readers have access to repos with the exact same live records, the ECMH digests are the same but the commits are different because each reader receives a distinct random HMAC key: 41 + 42 + ``` 43 + Reader A receives: 44 + ecmh = 0x7a3f... (deterministic, same for identical sets) 45 + hmac_key = 0x91ab... (random, unique to this reader) 46 + hmac = HMAC-SHA256(0x91ab..., 0x7a3f...) → 0xd4e7... 47 + sig = sign(ecmh || hmac || hmac_key) 48 + 49 + Reader B receives: 50 + ecmh = 0x7a3f... (same ECMH — same records) 51 + hmac_key = 0xc30f... (different random key) 52 + hmac = HMAC-SHA256(0xc30f..., 0x7a3f...) → 0x18b2... 53 + sig = sign(ecmh || hmac || hmac_key) 54 + ``` 55 + 56 + The two commits share the same ECMH value, but the HMACs are completely different. A third party comparing the two commits cannot determine whether the underlying repos are identical without independently knowing the record sets. 57 + 58 + ### Deniability 59 + 60 + The HMAC key is included in the commit. This is intentional. Because anyone who holds the key can produce a valid HMAC over any ECMH digest, a leaked commit does not serve as cryptographic proof that the signer sent that specific content to that specific reader. The signature covers the bundle, but the HMAC key's presence means the reader could have constructed the HMAC themselves. 61 + 62 + This gives the signer plausible deniability: the commit authenticates the data for the intended reader, but is not a transferable proof to third parties. 63 + 64 + ### No rebroadcast 65 + 66 + Each commit is authenticated for exactly one party. If client-a forwards their commit to client-b, client-b has no reason to trust it: the HMAC key was chosen for client-a, and client-b has their own key. The server will never produce the same HMAC key for two different readers. 67 + 68 + ## Demonstration 69 + 70 + The following shows how the ECMH layer fits into the commit protocol. The HMAC and signature operations use standard cryptographic primitives alongside the `ecmh` crate. 71 + 72 + ```rust 73 + use ecmh::RistrettoEcmh; 74 + use hmac::{Hmac, Mac}; 75 + use sha2::Sha256; 76 + 77 + type HmacSha256 = Hmac<Sha256>; 78 + 79 + /// A permissioned repo commit. 80 + struct Commit { 81 + ecmh: Vec<u8>, 82 + hmac: Vec<u8>, 83 + hmac_key: Vec<u8>, 84 + sig: Vec<u8>, 85 + } 86 + 87 + // --- Server side --- 88 + 89 + // The repo's live record set, maintained incrementally. 90 + let mut repo = RistrettoEcmh::new(); 91 + repo.insert(b"at://did:plc:abc/app.bsky.feed.post/3abc"); 92 + repo.insert(b"at://did:plc:abc/app.bsky.feed.post/3def"); 93 + repo.insert(b"at://did:plc:abc/app.bsky.feed.like/3ghi"); 94 + 95 + let ecmh_bytes = repo.to_bytes(); 96 + 97 + // Generate a commit for reader A. 98 + let hmac_key_a: [u8; 32] = rand::random(); 99 + let mut mac_a = HmacSha256::new_from_slice(&hmac_key_a).unwrap(); 100 + mac_a.update(&ecmh_bytes); 101 + let hmac_a = mac_a.finalize().into_bytes(); 102 + 103 + // Generate a commit for reader B — same repo, different key. 104 + let hmac_key_b: [u8; 32] = rand::random(); 105 + let mut mac_b = HmacSha256::new_from_slice(&hmac_key_b).unwrap(); 106 + mac_b.update(&ecmh_bytes); 107 + let hmac_b = mac_b.finalize().into_bytes(); 108 + 109 + // The ECMH digests are identical (same live records)... 110 + assert_eq!(ecmh_bytes, repo.to_bytes()); 111 + 112 + // ...but the HMACs are different (different transient keys). 113 + assert_ne!(hmac_a.as_slice(), hmac_b.as_slice()); 114 + 115 + // Each commit is then signed by the repo owner's atproto signing key 116 + // and delivered to the respective reader. 117 + ``` 118 + 119 + ### Verification by the reader 120 + 121 + The reader verifies the commit by: 122 + 123 + 1. Checking the signature against the repo owner's public key 124 + 2. Recomputing the HMAC from the provided key and ECMH digest 125 + 3. Comparing the recomputed HMAC to the one in the commit 126 + 4. Optionally, recomputing the ECMH from the received records to verify consistency 127 + 128 + ```rust 129 + // --- Reader A side --- 130 + 131 + // Reader A receives the commit and the record set from the server. 132 + let received_ecmh = &ecmh_bytes; 133 + let received_hmac = hmac_a.as_slice(); 134 + let received_key = &hmac_key_a; 135 + 136 + // Step 1: verify signature (omitted — use atproto signing key verification) 137 + 138 + // Step 2: recompute HMAC 139 + let mut verifier = HmacSha256::new_from_slice(received_key).unwrap(); 140 + verifier.update(received_ecmh); 141 + verifier.verify_slice(received_hmac).expect("HMAC mismatch"); 142 + 143 + // Step 3: recompute ECMH from the records to check consistency 144 + let mut local = RistrettoEcmh::new(); 145 + local.insert(b"at://did:plc:abc/app.bsky.feed.post/3abc"); 146 + local.insert(b"at://did:plc:abc/app.bsky.feed.post/3def"); 147 + local.insert(b"at://did:plc:abc/app.bsky.feed.like/3ghi"); 148 + assert_eq!(local.to_bytes(), *received_ecmh); 149 + ``` 150 + 151 + ### Incremental sync 152 + 153 + When records are added or removed, the server updates the ECMH incrementally and issues a new commit with a fresh HMAC key: 154 + 155 + ```rust 156 + // Server adds a new record 157 + repo.insert(b"at://did:plc:abc/app.bsky.feed.post/3jkl"); 158 + 159 + // Server deletes an old record 160 + repo.remove(b"at://did:plc:abc/app.bsky.feed.like/3ghi"); 161 + 162 + // New commit for reader A with a fresh key 163 + let new_ecmh = repo.to_bytes(); 164 + let new_hmac_key: [u8; 32] = rand::random(); 165 + let mut mac = HmacSha256::new_from_slice(&new_hmac_key).unwrap(); 166 + mac.update(&new_ecmh); 167 + let new_hmac = mac.finalize().into_bytes(); 168 + 169 + // The new ECMH reflects the updated record set. 170 + // The new HMAC key ensures this commit is distinct from all prior commits, 171 + // even if the record set happens to return to a previous state. 172 + ``` 173 + 174 + ## Security properties 175 + 176 + | Property | Mechanism | 177 + |---|---| 178 + | **Integrity** | ECMH digest changes if any record is added or removed | 179 + | **Consistency** | Reader recomputes ECMH from received records and compares to commit | 180 + | **Confidentiality** | ECMH is one-way; digest reveals nothing about records | 181 + | **Reader isolation** | Transient HMAC key makes each commit unique per reader | 182 + | **Deniability** | Included HMAC key means anyone could have produced the HMAC | 183 + | **No rebroadcast** | Commit is meaningful only to the reader who holds the matching key | 184 + | **Authenticity** | Signature by the repo owner's atproto signing key binds the commit | 185 + 186 + ## Limitations 187 + 188 + **ECMH equality across readers.** The raw ECMH field is the same for identical repos. The HMAC prevents trivial comparison of commits, but a reader who strips the HMAC and compares raw ECMH values with another reader can determine set equality. If this is a concern, the ECMH digest itself can be encrypted or omitted from the wire format, transmitting only the HMAC. 189 + 190 + **Small record universes.** If the space of possible records is small enough to enumerate, an attacker can brute-force the ECMH by hashing all possible subsets. ECMH security relies on the record space being large. 191 + 192 + **HMAC key lifecycle.** The HMAC key must be generated fresh for every read and never reused. Reusing a key across readers or across time would allow the same comparison attacks the HMAC is designed to prevent. 193 + 194 + **No partial proofs.** Unlike a Merkle tree, ECMH does not support proving that a single record is or is not in the set without revealing the entire set. The tradeoff is significantly lower computational and storage overhead.
+3 -1
src/lib.rs
··· 41 41 42 42 pub mod backend; 43 43 pub mod curves; 44 + #[cfg(feature = "wasm")] 45 + pub mod wasm; 44 46 45 47 pub use backend::CurveBackend; 46 48 #[cfg(feature = "p256")] ··· 52 54 53 55 use core::fmt; 54 56 use core::marker::PhantomData; 55 - use std::ops::{Add, AddAssign, Sub, SubAssign}; 57 + use core::ops::{Add, AddAssign, Sub, SubAssign}; 56 58 57 59 /// A homomorphic multiset hash value parameterized by a curve backend. 58 60 ///
+279
src/wasm.rs
··· 1 + //! WebAssembly bindings for ECMH via `wasm-bindgen`. 2 + //! 3 + //! Enabled by the `wasm` feature flag. Provides JS-friendly wrapper classes 4 + //! for each curve backend. 5 + 6 + use wasm_bindgen::prelude::*; 7 + 8 + // --------------------------------------------------------------------------- 9 + // Ristretto255 (always available when wasm feature is enabled) 10 + // --------------------------------------------------------------------------- 11 + 12 + /// Elliptic Curve Multiset Hash using Ristretto255. 13 + /// 14 + /// A homomorphic hash for multisets: the hash of the union of two sets 15 + /// equals the sum of their individual hashes. 16 + #[wasm_bindgen(js_name = "EcmhRistretto")] 17 + pub struct WasmEcmh { 18 + inner: crate::RistrettoEcmh, 19 + } 20 + 21 + impl Default for WasmEcmh { 22 + fn default() -> Self { 23 + Self::new() 24 + } 25 + } 26 + 27 + #[wasm_bindgen(js_class = "EcmhRistretto")] 28 + impl WasmEcmh { 29 + /// Create a hash representing the empty multiset. 30 + #[wasm_bindgen(constructor)] 31 + pub fn new() -> Self { 32 + Self { 33 + inner: crate::RistrettoEcmh::new(), 34 + } 35 + } 36 + 37 + /// Create a hash from a single element. 38 + #[wasm_bindgen(js_name = "fromElement")] 39 + pub fn from_element(data: &[u8]) -> Self { 40 + Self { 41 + inner: crate::RistrettoEcmh::from_element(data), 42 + } 43 + } 44 + 45 + /// Insert an element into the multiset. 46 + pub fn insert(&mut self, data: &[u8]) { 47 + self.inner.insert(data); 48 + } 49 + 50 + /// Remove an element from the multiset. 51 + pub fn remove(&mut self, data: &[u8]) { 52 + self.inner.remove(data); 53 + } 54 + 55 + /// Return the union of this hash with another. 56 + pub fn union(&self, other: &WasmEcmh) -> Self { 57 + Self { 58 + inner: self.inner.union(&other.inner), 59 + } 60 + } 61 + 62 + /// Return the difference of this hash minus another. 63 + pub fn difference(&self, other: &WasmEcmh) -> Self { 64 + Self { 65 + inner: self.inner.difference(&other.inner), 66 + } 67 + } 68 + 69 + /// Returns `true` if this hash represents the empty multiset. 70 + #[wasm_bindgen(js_name = "isEmpty")] 71 + pub fn is_empty(&self) -> bool { 72 + self.inner.is_empty() 73 + } 74 + 75 + /// Serialize the hash to bytes (Uint8Array in JS). 76 + #[wasm_bindgen(js_name = "toBytes")] 77 + pub fn to_bytes(&self) -> Vec<u8> { 78 + self.inner.to_bytes() 79 + } 80 + 81 + /// Deserialize a hash from bytes. Returns `undefined` if invalid. 82 + #[wasm_bindgen(js_name = "fromBytes")] 83 + pub fn from_bytes(bytes: &[u8]) -> Option<WasmEcmh> { 84 + crate::RistrettoEcmh::from_bytes(bytes).map(|inner| Self { inner }) 85 + } 86 + 87 + /// Return the hash as a hex string. 88 + #[wasm_bindgen(js_name = "toHex")] 89 + pub fn to_hex(&self) -> String { 90 + hex::encode(self.inner.to_bytes()) 91 + } 92 + 93 + /// Check equality with another hash. 94 + pub fn equals(&self, other: &WasmEcmh) -> bool { 95 + self.inner == other.inner 96 + } 97 + } 98 + 99 + // --------------------------------------------------------------------------- 100 + // P-256 101 + // --------------------------------------------------------------------------- 102 + 103 + #[cfg(feature = "p256")] 104 + /// Elliptic Curve Multiset Hash using NIST P-256. 105 + #[wasm_bindgen(js_name = "EcmhP256")] 106 + pub struct WasmEcmhP256 { 107 + inner: crate::P256Ecmh, 108 + } 109 + 110 + #[cfg(feature = "p256")] 111 + impl Default for WasmEcmhP256 { 112 + fn default() -> Self { 113 + Self::new() 114 + } 115 + } 116 + 117 + #[cfg(feature = "p256")] 118 + #[wasm_bindgen(js_class = "EcmhP256")] 119 + impl WasmEcmhP256 { 120 + /// Create a hash representing the empty multiset. 121 + #[wasm_bindgen(constructor)] 122 + pub fn new() -> Self { 123 + Self { 124 + inner: crate::P256Ecmh::new(), 125 + } 126 + } 127 + 128 + /// Create a hash from a single element. 129 + #[wasm_bindgen(js_name = "fromElement")] 130 + pub fn from_element(data: &[u8]) -> Self { 131 + Self { 132 + inner: crate::P256Ecmh::from_element(data), 133 + } 134 + } 135 + 136 + /// Insert an element into the multiset. 137 + pub fn insert(&mut self, data: &[u8]) { 138 + self.inner.insert(data); 139 + } 140 + 141 + /// Remove an element from the multiset. 142 + pub fn remove(&mut self, data: &[u8]) { 143 + self.inner.remove(data); 144 + } 145 + 146 + /// Return the union of this hash with another. 147 + pub fn union(&self, other: &WasmEcmhP256) -> Self { 148 + Self { 149 + inner: self.inner.union(&other.inner), 150 + } 151 + } 152 + 153 + /// Return the difference of this hash minus another. 154 + pub fn difference(&self, other: &WasmEcmhP256) -> Self { 155 + Self { 156 + inner: self.inner.difference(&other.inner), 157 + } 158 + } 159 + 160 + /// Returns `true` if this hash represents the empty multiset. 161 + #[wasm_bindgen(js_name = "isEmpty")] 162 + pub fn is_empty(&self) -> bool { 163 + self.inner.is_empty() 164 + } 165 + 166 + /// Serialize the hash to bytes (Uint8Array in JS). 167 + #[wasm_bindgen(js_name = "toBytes")] 168 + pub fn to_bytes(&self) -> Vec<u8> { 169 + self.inner.to_bytes() 170 + } 171 + 172 + /// Deserialize a hash from bytes. Returns `undefined` if invalid. 173 + #[wasm_bindgen(js_name = "fromBytes")] 174 + pub fn from_bytes(bytes: &[u8]) -> Option<WasmEcmhP256> { 175 + crate::P256Ecmh::from_bytes(bytes).map(|inner| Self { inner }) 176 + } 177 + 178 + /// Return the hash as a hex string. 179 + #[wasm_bindgen(js_name = "toHex")] 180 + pub fn to_hex(&self) -> String { 181 + hex::encode(self.inner.to_bytes()) 182 + } 183 + 184 + /// Check equality with another hash. 185 + pub fn equals(&self, other: &WasmEcmhP256) -> bool { 186 + self.inner == other.inner 187 + } 188 + } 189 + 190 + // --------------------------------------------------------------------------- 191 + // secp256k1 (K-256) 192 + // --------------------------------------------------------------------------- 193 + 194 + #[cfg(feature = "k256")] 195 + /// Elliptic Curve Multiset Hash using secp256k1. 196 + #[wasm_bindgen(js_name = "EcmhK256")] 197 + pub struct WasmEcmhK256 { 198 + inner: crate::K256Ecmh, 199 + } 200 + 201 + #[cfg(feature = "k256")] 202 + impl Default for WasmEcmhK256 { 203 + fn default() -> Self { 204 + Self::new() 205 + } 206 + } 207 + 208 + #[cfg(feature = "k256")] 209 + #[wasm_bindgen(js_class = "EcmhK256")] 210 + impl WasmEcmhK256 { 211 + /// Create a hash representing the empty multiset. 212 + #[wasm_bindgen(constructor)] 213 + pub fn new() -> Self { 214 + Self { 215 + inner: crate::K256Ecmh::new(), 216 + } 217 + } 218 + 219 + /// Create a hash from a single element. 220 + #[wasm_bindgen(js_name = "fromElement")] 221 + pub fn from_element(data: &[u8]) -> Self { 222 + Self { 223 + inner: crate::K256Ecmh::from_element(data), 224 + } 225 + } 226 + 227 + /// Insert an element into the multiset. 228 + pub fn insert(&mut self, data: &[u8]) { 229 + self.inner.insert(data); 230 + } 231 + 232 + /// Remove an element from the multiset. 233 + pub fn remove(&mut self, data: &[u8]) { 234 + self.inner.remove(data); 235 + } 236 + 237 + /// Return the union of this hash with another. 238 + pub fn union(&self, other: &WasmEcmhK256) -> Self { 239 + Self { 240 + inner: self.inner.union(&other.inner), 241 + } 242 + } 243 + 244 + /// Return the difference of this hash minus another. 245 + pub fn difference(&self, other: &WasmEcmhK256) -> Self { 246 + Self { 247 + inner: self.inner.difference(&other.inner), 248 + } 249 + } 250 + 251 + /// Returns `true` if this hash represents the empty multiset. 252 + #[wasm_bindgen(js_name = "isEmpty")] 253 + pub fn is_empty(&self) -> bool { 254 + self.inner.is_empty() 255 + } 256 + 257 + /// Serialize the hash to bytes (Uint8Array in JS). 258 + #[wasm_bindgen(js_name = "toBytes")] 259 + pub fn to_bytes(&self) -> Vec<u8> { 260 + self.inner.to_bytes() 261 + } 262 + 263 + /// Deserialize a hash from bytes. Returns `undefined` if invalid. 264 + #[wasm_bindgen(js_name = "fromBytes")] 265 + pub fn from_bytes(bytes: &[u8]) -> Option<WasmEcmhK256> { 266 + crate::K256Ecmh::from_bytes(bytes).map(|inner| Self { inner }) 267 + } 268 + 269 + /// Return the hash as a hex string. 270 + #[wasm_bindgen(js_name = "toHex")] 271 + pub fn to_hex(&self) -> String { 272 + hex::encode(self.inner.to_bytes()) 273 + } 274 + 275 + /// Check equality with another hash. 276 + pub fn equals(&self, other: &WasmEcmhK256) -> bool { 277 + self.inner == other.inner 278 + } 279 + }