From fac2e49cf133e4de10f5b4ea91a9e179dce1a28e Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 20 Apr 2026 17:37:50 -0400 Subject: [PATCH] feat(wasm): manifest / item / settings encrypt+decrypt via SessionHandle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds six #[wasm_bindgen] functions (manifest_encrypt/decrypt, item_encrypt/decrypt, settings_encrypt/decrypt) plus a native round-trip test that verifies encrypt→core_decrypt and nonce uniqueness without calling js-sys (serde_wasm_bindgen::from_value is wasm32-only; documented in test comment). --- crates/relicario-wasm/src/lib.rs | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 51539b9..f3b5a2e 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -46,6 +46,75 @@ pub fn lock(handle: &SessionHandle) -> bool { // Subsequent wasm_bindgen fns added in Tasks 19-21. +use serde_wasm_bindgen::to_value; +use relicario_core::{ + decrypt_item, decrypt_manifest, decrypt_settings, + encrypt_item, encrypt_manifest, encrypt_settings, + Item, Manifest, VaultSettings, +}; + +fn need_key(handle: &SessionHandle) -> Result<(), JsError> { + if session::with(handle.0, |_| ()).is_some() { Ok(()) } + else { Err(JsError::new("invalid or locked session handle")) } +} + +#[wasm_bindgen] +pub fn manifest_decrypt(handle: &SessionHandle, encrypted: &[u8]) -> Result { + need_key(handle)?; + let out = session::with(handle.0, |k| decrypt_manifest(encrypted, k)) + .unwrap() + .map_err(|e| JsError::new(&e.to_string()))?; + to_value(&out).map_err(|e| JsError::new(&e.to_string())) +} + +#[wasm_bindgen] +pub fn manifest_encrypt(handle: &SessionHandle, manifest_json: &str) -> Result, JsError> { + need_key(handle)?; + let m: Manifest = serde_json::from_str(manifest_json) + .map_err(|e| JsError::new(&format!("manifest json: {e}")))?; + session::with(handle.0, |k| encrypt_manifest(&m, k)) + .unwrap() + .map_err(|e| JsError::new(&e.to_string())) +} + +#[wasm_bindgen] +pub fn item_decrypt(handle: &SessionHandle, encrypted: &[u8]) -> Result { + need_key(handle)?; + let out = session::with(handle.0, |k| decrypt_item(encrypted, k)) + .unwrap() + .map_err(|e| JsError::new(&e.to_string()))?; + to_value(&out).map_err(|e| JsError::new(&e.to_string())) +} + +#[wasm_bindgen] +pub fn item_encrypt(handle: &SessionHandle, item_json: &str) -> Result, JsError> { + need_key(handle)?; + let item: Item = serde_json::from_str(item_json) + .map_err(|e| JsError::new(&format!("item json: {e}")))?; + session::with(handle.0, |k| encrypt_item(&item, k)) + .unwrap() + .map_err(|e| JsError::new(&e.to_string())) +} + +#[wasm_bindgen] +pub fn settings_decrypt(handle: &SessionHandle, encrypted: &[u8]) -> Result { + need_key(handle)?; + let out = session::with(handle.0, |k| decrypt_settings(encrypted, k)) + .unwrap() + .map_err(|e| JsError::new(&e.to_string()))?; + to_value(&out).map_err(|e| JsError::new(&e.to_string())) +} + +#[wasm_bindgen] +pub fn settings_encrypt(handle: &SessionHandle, settings_json: &str) -> Result, JsError> { + need_key(handle)?; + let s: VaultSettings = serde_json::from_str(settings_json) + .map_err(|e| JsError::new(&format!("settings json: {e}")))?; + session::with(handle.0, |k| encrypt_settings(&s, k)) + .unwrap() + .map_err(|e| JsError::new(&e.to_string())) +} + #[cfg(test)] mod session_tests { use super::*; @@ -70,4 +139,22 @@ mod session_tests { let byte = session::with(h, |k| k[0]); assert_eq!(byte, None); } + + #[test] + fn manifest_round_trip_via_handle() { + use relicario_core::{Manifest, decrypt_manifest}; + session::clear(); + let h = session::insert(Zeroizing::new([0x55u8; 32])); + let handle = SessionHandle(h); + let key = Zeroizing::new([0x55u8; 32]); + let empty = Manifest::new(); + let bytes = manifest_encrypt(&handle, &serde_json::to_string(&empty).unwrap()).unwrap(); + assert!(!bytes.is_empty()); + // Decrypt via core directly (avoids js-sys on native). + let parsed: Manifest = decrypt_manifest(&bytes, &key).unwrap(); + assert_eq!(parsed.items.len(), 0); + // Random nonces mean two encryptions of the same plaintext differ. + let bytes2 = manifest_encrypt(&handle, &serde_json::to_string(&empty).unwrap()).unwrap(); + assert_ne!(bytes, bytes2, "nonces must differ"); + } }