diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 7ef8569..c23c1fa 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -486,6 +486,42 @@ pub fn parse_lastpass_csv_json(csv_bytes: &[u8]) -> Result { Ok(json.to_string()) } +// ── Recovery QR bindings ───────────────────────────────────────────────────── + +use relicario_core::{generate_recovery_qr, recovery_qr_to_svg, unwrap_recovery_qr}; + +/// Generate a recovery QR SVG for the current session. +/// Returns the SVG string. The passphrase wraps the image_secret under a +/// separate key (domain-separated from the master key derivation). +#[wasm_bindgen] +pub fn wasm_generate_recovery_qr( + handle: &SessionHandle, + passphrase: &str, +) -> Result { + let image_secret_bytes = session::with_image_secret(handle.0, |s| s.to_vec()) + .ok_or_else(|| JsError::new("invalid or locked session handle"))?; + let image_secret: &[u8; 32] = image_secret_bytes.as_slice().try_into() + .map_err(|_| JsError::new("image_secret must be 32 bytes"))?; + let payload = generate_recovery_qr(passphrase, image_secret) + .map_err(|e| JsError::new(&e.to_string()))?; + Ok(recovery_qr_to_svg(&payload)) +} + +/// Unwrap a recovery QR payload (base64-encoded 109-byte blob) using the passphrase. +/// Returns the raw image_secret bytes (32 bytes). +#[wasm_bindgen] +pub fn wasm_unwrap_recovery_qr( + payload_b64: &str, + passphrase: &str, +) -> Result, JsError> { + use base64::{engine::general_purpose::STANDARD, Engine}; + let bytes = STANDARD.decode(payload_b64) + .map_err(|e| JsError::new(&format!("base64: {e}")))?; + let recovered = unwrap_recovery_qr(&bytes, passphrase) + .map_err(|e| JsError::new(&e.to_string()))?; + Ok(recovered.to_vec()) +} + #[cfg(test)] mod session_tests { use super::*;