From 7407fe512f7086165541a4baf3cc9243371d8435 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Tue, 28 Apr 2026 19:52:36 -0400 Subject: [PATCH] feat(wasm): pack_backup_json / unpack_backup_json JSON bridge for the SW. Binary fields are base64 in the JSON wrapper; core gets borrowed byte slices. --- crates/relicario-wasm/src/lib.rs | 127 +++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 872beb4..4657e58 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -305,6 +305,133 @@ pub fn totp_compute( Ok(TotpCode { code, expires_at }) } +// ── Backup container bridge ───────────────────────────────────────────────── + +use relicario_core::backup::{ + pack_backup as core_pack_backup, + unpack_backup as core_unpack_backup, + BackupInput, BackupItem, BackupAttachment, +}; + +/// Pack a vault into a `.relbak` byte vector. +/// +/// `input_json` shape: +/// ```json +/// { +/// "salt": "", +/// "params_json": "...", +/// "devices_json": "...", +/// "manifest_enc": "", +/// "settings_enc": "", +/// "items": [{"id": "", "ciphertext": ""}, ...], +/// "attachments": [{"item_id": "", "attachment_id": "", "ciphertext": ""}, ...], +/// "reference_jpg": "" | null, +/// "git_archive": "" | null +/// } +/// ``` +#[wasm_bindgen] +pub fn pack_backup_json(input_json: &str, passphrase: &str) -> Result, JsError> { + #[derive(serde::Deserialize)] + struct InJson { + salt: String, + params_json: String, + devices_json: String, + manifest_enc: String, + settings_enc: String, + items: Vec, + attachments: Vec, + reference_jpg: Option, + git_archive: Option, + } + #[derive(serde::Deserialize)] + struct InItem { id: String, ciphertext: String } + #[derive(serde::Deserialize)] + struct InAttachment { item_id: String, attachment_id: String, ciphertext: String } + + let parsed: InJson = serde_json::from_str(input_json) + .map_err(|e| JsError::new(&format!("backup input: {e}")))?; + + let b64 = base64::engine::general_purpose::STANDARD; + let salt = b64.decode(&parsed.salt).map_err(|e| JsError::new(&e.to_string()))?; + let manifest = b64.decode(&parsed.manifest_enc).map_err(|e| JsError::new(&e.to_string()))?; + let settings = b64.decode(&parsed.settings_enc).map_err(|e| JsError::new(&e.to_string()))?; + let items_bytes: Vec<(String, Vec)> = parsed.items.iter() + .map(|i| { + let ct = b64.decode(&i.ciphertext).map_err(|e| JsError::new(&e.to_string()))?; + Ok((i.id.clone(), ct)) + }) + .collect::, JsError>>()?; + let attach_bytes: Vec<(String, String, Vec)> = parsed.attachments.iter() + .map(|a| { + let ct = b64.decode(&a.ciphertext).map_err(|e| JsError::new(&e.to_string()))?; + Ok((a.item_id.clone(), a.attachment_id.clone(), ct)) + }) + .collect::, JsError>>()?; + + let ref_bytes = parsed.reference_jpg.as_deref() + .map(|s| b64.decode(s)) + .transpose() + .map_err(|e| JsError::new(&e.to_string()))?; + let git_bytes = parsed.git_archive.as_deref() + .map(|s| b64.decode(s)) + .transpose() + .map_err(|e| JsError::new(&e.to_string()))?; + + let items_refs: Vec = items_bytes.iter() + .map(|(id, ct)| BackupItem { id: id.clone(), ciphertext: ct }) + .collect(); + let attach_refs: Vec = attach_bytes.iter() + .map(|(iid, aid, ct)| BackupAttachment { + item_id: iid.clone(), + attachment_id: aid.clone(), + ciphertext: ct, + }) + .collect(); + + let input = BackupInput { + salt: &salt, + params_json: &parsed.params_json, + devices_json: &parsed.devices_json, + manifest_enc: &manifest, + settings_enc: &settings, + items: items_refs, + attachments: attach_refs, + reference_jpg: ref_bytes.as_deref(), + git_archive: git_bytes.as_deref(), + }; + core_pack_backup(input, passphrase).map_err(|e| JsError::new(&e.to_string())) +} + +/// Unpack `.relbak` bytes; returns the JSON shape that mirrors `BackupOutput`, +/// with binary fields base64-encoded. +#[wasm_bindgen] +pub fn unpack_backup_json(bytes: &[u8], passphrase: &str) -> Result { + let out = core_unpack_backup(bytes, passphrase) + .map_err(|e| JsError::new(&e.to_string()))?; + + let b64 = base64::engine::general_purpose::STANDARD; + let json = serde_json::json!({ + "salt": b64.encode(out.salt), + "params_json": out.params_json, + "devices_json": out.devices_json, + "manifest_enc": b64.encode(&out.manifest_enc), + "settings_enc": b64.encode(&out.settings_enc), + "items": out.items.iter().map(|i| serde_json::json!({ + "id": i.id, + "ciphertext": b64.encode(&i.ciphertext), + })).collect::>(), + "attachments": out.attachments.iter().map(|a| serde_json::json!({ + "item_id": a.item_id, + "attachment_id": a.attachment_id, + "ciphertext": b64.encode(&a.ciphertext), + })).collect::>(), + "reference_jpg": out.reference_jpg.as_ref().map(|b| b64.encode(b)), + "git_archive": out.git_archive.as_ref().map(|b| b64.encode(b)), + "created_at": out.created_at, + }); + Ok(json.to_string()) +} + #[cfg(test)] mod session_tests { use super::*;