use std::collections::BTreeMap;
use serde::Deserialize;
use wasm_bindgen::prelude::*;
use zeroize::Zeroizing;
use crate::serde_bridge::*;
const MAX_WASM_AUTHORIZED_TRUSTEES: usize = 1_024;
#[derive(Deserialize)]
struct WasmAuthorizedTrustee {
did: String,
public_key_hex: String,
}
#[wasm_bindgen]
pub fn wasm_generate_x25519_keypair() -> Result<JsValue, JsValue> {
Err(JsValue::from_str(
"X25519 key generation is disabled at the WASM boundary; use external key management and pass caller-supplied ephemeral material to wasm_prepare_encrypted_message",
))
}
#[wasm_bindgen]
pub fn wasm_x25519_public_from_secret(_secret_hex: &str) -> Result<JsValue, JsValue> {
Err(JsValue::from_str(
"raw X25519 secret public derivation is disabled at the WASM boundary; derive public keys in external key management before calling WASM",
))
}
#[wasm_bindgen]
pub fn wasm_caller_managed_x25519_public_from_secret(secret_hex: &str) -> Result<JsValue, JsValue> {
let secret = exo_messaging::X25519SecretKey::from_hex(secret_hex)
.map_err(|e| JsValue::from_str(&format!("invalid caller-managed X25519 secret: {e}")))?;
let public = secret.public_key();
to_js_value(&serde_json::json!({
"public_key_hex": public.to_hex(),
}))
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn wasm_encrypt_message(
plaintext: &str,
content_type_json: &str,
sender_did: &str,
recipient_did: &str,
_legacy_sender_key_hex: &str,
recipient_x25519_public_hex: &str,
message_id: &str,
created_physical_ms: u64,
created_logical: u32,
release_on_death: bool,
release_delay_hours: u32,
) -> Result<JsValue, JsValue> {
let _ = (
plaintext,
content_type_json,
sender_did,
recipient_did,
recipient_x25519_public_hex,
message_id,
created_physical_ms,
created_logical,
release_on_death,
release_delay_hours,
);
Err(JsValue::from_str(
"raw Ed25519 sender signing is disabled at the WASM boundary; call wasm_prepare_encrypted_message with caller-supplied ephemeral X25519 material, sign externally, then call wasm_attach_message_signature",
))
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn wasm_prepare_encrypted_message(
plaintext: &str,
content_type_json: &str,
sender_did: &str,
recipient_did: &str,
recipient_x25519_public_hex: &str,
ephemeral_x25519_secret_hex: &str,
message_id: &str,
created_physical_ms: u64,
created_logical: u32,
release_on_death: bool,
release_delay_hours: u32,
) -> Result<JsValue, JsValue> {
let content_type: exo_messaging::ContentType = from_json_str(content_type_json)?;
let sender = exo_core::Did::new(sender_did)
.map_err(|e| JsValue::from_str(&format!("invalid sender DID: {e}")))?;
let recipient = exo_core::Did::new(recipient_did)
.map_err(|e| JsValue::from_str(&format!("invalid recipient DID: {e}")))?;
let recipient_pub = exo_messaging::X25519PublicKey::from_hex(recipient_x25519_public_hex)
.map_err(|e| JsValue::from_str(&format!("invalid recipient X25519 key: {e}")))?;
let ephemeral_keypair =
parse_x25519_keypair_hex("ephemeral X25519 secret", ephemeral_x25519_secret_hex)?;
let message_uuid = uuid::Uuid::parse_str(message_id)
.map_err(|e| JsValue::from_str(&format!("invalid message id: {e}")))?;
let metadata = exo_messaging::ComposeMetadata::new(
message_uuid,
exo_core::Timestamp::new(created_physical_ms, created_logical),
)
.map_err(|e| JsValue::from_str(&format!("invalid envelope metadata: {e}")))?;
let envelope = exo_messaging::prepare_envelope_for_signing_with_ephemeral(
plaintext.as_bytes(),
content_type,
&sender,
&recipient,
&recipient_pub,
&ephemeral_keypair,
metadata,
release_on_death,
release_delay_hours,
)
.map_err(|e| JsValue::from_str(&format!("encryption failed: {e}")))?;
let signing_payload = envelope
.signing_payload()
.map_err(|e| JsValue::from_str(&format!("signature payload failed: {e}")))?;
to_js_value(&serde_json::json!({
"envelope": envelope,
"signing_payload_hex": hex::encode(signing_payload),
}))
}
fn parse_x25519_keypair_hex(
label: &str,
secret_hex: &str,
) -> Result<exo_messaging::X25519KeyPair, JsValue> {
let bytes = Zeroizing::new(
hex::decode(secret_hex)
.map_err(|e| JsValue::from_str(&format!("{label} must be hex: {e}")))?,
);
if bytes.len() != 32 {
return Err(JsValue::from_str(&format!("{label} must be 32 bytes")));
}
let mut secret = Zeroizing::new([0u8; 32]);
secret.copy_from_slice(bytes.as_slice());
exo_messaging::X25519KeyPair::from_secret_bytes(*secret)
.map_err(|e| JsValue::from_str(&format!("invalid {label}: {e}")))
}
#[wasm_bindgen]
pub fn wasm_attach_message_signature(
envelope_json: &str,
sender_ed25519_public_hex: &str,
signature_hex: &str,
) -> Result<JsValue, JsValue> {
let envelope: exo_messaging::EncryptedEnvelope = from_json_str(envelope_json)?;
let sender_public =
parse_ed25519_public_key_hex("sender Ed25519 public key", sender_ed25519_public_hex)?;
let signature = parse_ed25519_signature_hex("sender envelope signature", signature_hex)?;
let envelope = exo_messaging::attach_verified_signature(envelope, signature, &sender_public)
.map_err(|e| JsValue::from_str(&format!("signature attachment failed: {e}")))?;
to_js_value(&envelope)
}
#[wasm_bindgen]
pub fn wasm_decrypt_message(
envelope_json: &str,
recipient_x25519_secret_hex: &str,
sender_ed25519_public_hex: &str,
) -> Result<JsValue, JsValue> {
let envelope: exo_messaging::EncryptedEnvelope = from_json_str(envelope_json)?;
let recipient_secret = exo_messaging::X25519SecretKey::from_hex(recipient_x25519_secret_hex)
.map_err(|e| JsValue::from_str(&format!("invalid recipient secret: {e}")))?;
let pk_bytes = hex::decode(sender_ed25519_public_hex)
.map_err(|e| JsValue::from_str(&format!("invalid sender public key hex: {e}")))?;
if pk_bytes.len() != 32 {
return Err(JsValue::from_str(
"sender Ed25519 public key must be 32 bytes",
));
}
let mut pk_arr = [0u8; 32];
pk_arr.copy_from_slice(&pk_bytes);
let sender_pk = exo_core::PublicKey::from_bytes(pk_arr);
let plaintext = exo_messaging::unlock(&envelope, &recipient_secret, &sender_pk)
.map_err(|e| JsValue::from_str(&format!("decryption failed: {e}")))?;
let plaintext_str = String::from_utf8(plaintext)
.map_err(|e| JsValue::from_str(&format!("plaintext is not valid UTF-8: {e}")))?;
to_js_value(&serde_json::json!({
"plaintext": plaintext_str,
"content_type": envelope.content_type,
}))
}
#[wasm_bindgen]
pub fn wasm_verify_message_signature(
envelope_json: &str,
sender_ed25519_public_hex: &str,
) -> Result<bool, JsValue> {
let envelope: exo_messaging::EncryptedEnvelope = from_json_str(envelope_json)?;
let pk_bytes = hex::decode(sender_ed25519_public_hex)
.map_err(|e| JsValue::from_str(&format!("invalid public key hex: {e}")))?;
if pk_bytes.len() != 32 {
return Err(JsValue::from_str("public key must be 32 bytes"));
}
let mut pk_arr = [0u8; 32];
pk_arr.copy_from_slice(&pk_bytes);
let sender_pk = exo_core::PublicKey::from_bytes(pk_arr);
let signable = envelope
.signing_payload()
.map_err(|e| JsValue::from_str(&format!("signature payload failed: {e}")))?;
Ok(exo_core::crypto::verify(
&signable,
&envelope.signature,
&sender_pk,
))
}
fn parse_ed25519_public_key_hex(label: &str, value: &str) -> Result<exo_core::PublicKey, JsValue> {
let bytes =
hex::decode(value).map_err(|e| JsValue::from_str(&format!("invalid {label} hex: {e}")))?;
if bytes.len() != 32 {
return Err(JsValue::from_str(&format!("{label} must be 32 bytes")));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(exo_core::PublicKey::from_bytes(arr))
}
fn parse_ed25519_signature_hex(label: &str, value: &str) -> Result<exo_core::Signature, JsValue> {
let bytes =
hex::decode(value).map_err(|e| JsValue::from_str(&format!("invalid {label} hex: {e}")))?;
if bytes.len() != 64 {
return Err(JsValue::from_str(&format!("{label} must be 64 bytes")));
}
let mut arr = [0u8; 64];
arr.copy_from_slice(&bytes);
Ok(exo_core::Signature::from_bytes(arr))
}
fn parse_authorized_trustees_json(
authorized_trustees_json: &str,
) -> Result<BTreeMap<exo_core::Did, exo_core::PublicKey>, JsValue> {
let trustees: Vec<WasmAuthorizedTrustee> = from_json_bounded_vec(
authorized_trustees_json,
"authorized trustees",
MAX_WASM_AUTHORIZED_TRUSTEES,
)?;
let mut authorized = BTreeMap::new();
for trustee in trustees {
let did = exo_core::Did::new(&trustee.did)
.map_err(|e| JsValue::from_str(&format!("invalid trustee DID: {e}")))?;
let public_key =
parse_ed25519_public_key_hex("trustee Ed25519 public key", &trustee.public_key_hex)?;
if authorized.insert(did.clone(), public_key).is_some() {
return Err(JsValue::from_str(&format!(
"duplicate authorized trustee: {}",
did.as_str()
)));
}
}
Ok(authorized)
}
#[wasm_bindgen]
pub fn wasm_death_verification_initial_signing_payload(
subject_did: &str,
initiated_by_did: &str,
required_confirmations: u8,
authorized_trustees_json: &str,
claim_nonce_hex: &str,
created_physical_ms: u64,
created_logical: u32,
) -> Result<Vec<u8>, JsValue> {
let subject = exo_core::Did::new(subject_did)
.map_err(|e| JsValue::from_str(&format!("invalid subject DID: {e}")))?;
let initiator = exo_core::Did::new(initiated_by_did)
.map_err(|e| JsValue::from_str(&format!("invalid initiator DID: {e}")))?;
let authorized_trustees = parse_authorized_trustees_json(authorized_trustees_json)?;
let claim_nonce = hex::decode(claim_nonce_hex)
.map_err(|e| JsValue::from_str(&format!("invalid claim nonce hex: {e}")))?;
let metadata = exo_messaging::death_trigger::DeathVerificationCreationMetadata::new(
exo_core::Timestamp::new(created_physical_ms, created_logical),
)
.map_err(|e| JsValue::from_str(&format!("invalid death verification metadata: {e}")))?;
exo_messaging::death_trigger::initial_confirmation_signing_payload(
&subject,
&initiator,
required_confirmations,
&authorized_trustees,
&claim_nonce,
&metadata.created_at,
)
.map_err(|e| JsValue::from_str(&format!("death verification signing payload failed: {e}")))
}
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn wasm_death_verification_new(
subject_did: &str,
initiated_by_did: &str,
required_confirmations: u8,
authorized_trustees_json: &str,
claim_nonce_hex: &str,
initiator_signature_hex: &str,
created_physical_ms: u64,
created_logical: u32,
) -> Result<JsValue, JsValue> {
let subject = exo_core::Did::new(subject_did)
.map_err(|e| JsValue::from_str(&format!("invalid subject DID: {e}")))?;
let initiator = exo_core::Did::new(initiated_by_did)
.map_err(|e| JsValue::from_str(&format!("invalid initiator DID: {e}")))?;
let authorized_trustees = parse_authorized_trustees_json(authorized_trustees_json)?;
let claim_nonce = hex::decode(claim_nonce_hex)
.map_err(|e| JsValue::from_str(&format!("invalid claim nonce hex: {e}")))?;
let initiator_signature =
parse_ed25519_signature_hex("initiator confirmation signature", initiator_signature_hex)?;
let metadata = exo_messaging::death_trigger::DeathVerificationCreationMetadata::new(
exo_core::Timestamp::new(created_physical_ms, created_logical),
)
.map_err(|e| JsValue::from_str(&format!("invalid death verification metadata: {e}")))?;
let dv = exo_messaging::death_trigger::DeathVerification::new(
subject,
initiator,
required_confirmations,
authorized_trustees,
claim_nonce,
initiator_signature,
metadata,
)
.map_err(|e| JsValue::from_str(&format!("death verification creation failed: {e}")))?;
to_js_value(&dv)
}
#[wasm_bindgen]
pub fn wasm_death_verification_confirmation_signing_payload(
state_json: &str,
trustee_did: &str,
confirmed_physical_ms: u64,
confirmed_logical: u32,
) -> Result<Vec<u8>, JsValue> {
let dv: exo_messaging::death_trigger::DeathVerification = from_json_str(state_json)?;
let trustee = exo_core::Did::new(trustee_did)
.map_err(|e| JsValue::from_str(&format!("invalid trustee DID: {e}")))?;
let metadata = exo_messaging::death_trigger::DeathConfirmationMetadata::new(
exo_core::Timestamp::new(confirmed_physical_ms, confirmed_logical),
)
.map_err(|e| JsValue::from_str(&format!("invalid death confirmation metadata: {e}")))?;
dv.confirmation_signing_payload(&trustee, &metadata.confirmed_at)
.map_err(|e| {
JsValue::from_str(&format!(
"death verification confirmation payload failed: {e}"
))
})
}
#[wasm_bindgen]
pub fn wasm_death_verification_confirm(
state_json: &str,
trustee_did: &str,
trustee_public_key_hex: &str,
signature_hex: &str,
confirmed_physical_ms: u64,
confirmed_logical: u32,
) -> Result<JsValue, JsValue> {
let mut dv: exo_messaging::death_trigger::DeathVerification = from_json_str(state_json)?;
let trustee = exo_core::Did::new(trustee_did)
.map_err(|e| JsValue::from_str(&format!("invalid trustee DID: {e}")))?;
let trustee_public_key =
parse_ed25519_public_key_hex("trustee Ed25519 public key", trustee_public_key_hex)?;
let signature = parse_ed25519_signature_hex("trustee confirmation signature", signature_hex)?;
let metadata = exo_messaging::death_trigger::DeathConfirmationMetadata::new(
exo_core::Timestamp::new(confirmed_physical_ms, confirmed_logical),
)
.map_err(|e| JsValue::from_str(&format!("invalid death confirmation metadata: {e}")))?;
let verified = dv
.confirm(trustee, trustee_public_key, signature, metadata)
.map_err(|e| JsValue::from_str(&format!("confirmation failed: {e}")))?;
to_js_value(&serde_json::json!({
"verified": verified,
"confirmations_remaining": dv.confirmations_remaining(),
"state": dv,
}))
}