[Fix] Harden MTP codec, transport, and SDK security

This commit is contained in:
Alex Emmet 2026-08-18 20:57:45 +02:00
commit a7e804c603
No known key found for this signature in database
73 changed files with 11892 additions and 5756 deletions

View file

@ -27,7 +27,7 @@ getrandom-v04 = { package = "getrandom", version = "0.4.3", features = ["wasm_js
mtp-common = { version = "0.3.0", path = "../common" }
mtp-type-map = { version = "0.3.0", path = "../type-map" }
mtp-codec = { version = "0.3.0", path = "../codec", features = ["crypto", "pipes", "registry"] }
mtp-crypto = { version = "0.3.0", path = "../crypto", features = ["wasm"] }
mtp-crypto = { version = "0.3.0", path = "../crypto", features = ["wasm", "password-kdf"] }
zeroize = "1.9"
wasm-bindgen-test = "0.3.76"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,630 @@
use wasm_bindgen::prelude::*;
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
use crate::auth;
use crate::client::{ConnectionState, WasmClient};
use crate::config::ConnectionConfig;
use crate::error::js_error;
use crate::transport::WasmTransport;
#[wasm_bindgen]
#[allow(deprecated)]
impl WasmClient {
pub async fn connect(&self, config: &ConnectionConfig) -> Result<(), JsValue> {
self.connect_owned(config.clone()).await
}
#[wasm_bindgen(js_name = connectOwned)]
pub async fn connect_owned(&self, config: ConnectionConfig) -> Result<(), JsValue> {
let generation = self.begin_connection();
let transport = match WasmTransport::connect_with_limits(
&config.url,
config.server_certificate_hashes.clone(),
config.max_message_size,
self.receive_decode_limits(),
)
.await
{
Ok(transport) => transport,
Err(error) => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
if !self.install_attempt_transport(&transport, generation) {
return Err(js_error("connection attempt superseded"));
}
let result = async {
let version_str = format!("{}", PROTOCOL_VERSION);
let opening_codec = mtp_codec::registry::VersionedCodec::for_version(
mtp_codec::registry::Registry::builtin(),
PROTOCOL_VERSION,
)
.ok_or_else(|| js_error("client protocol version is not registered"))?;
transport.set_type_map(opening_codec.type_map());
let mut ident = CommunicationValue::new_with_type_map(
CommunicationType::Identification,
opening_codec.type_map(),
)
.add_typed_default(DataType::Version, DataValue::Str(version_str))
.add_typed_default(
DataType::Id,
DataValue::UnsignedNumber(config.client_id as u128),
);
if let Some(desc) = &config.description {
ident =
ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
}
let ident_bytes = ident
.to_bytes()
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
transport.send_frame(&ident_bytes).await?;
let outcome_bytes = transport.read_one_frame().await?;
let outcome = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&outcome_bytes,
opening_codec.type_map(),
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse handshake outcome: {e}")))?;
if Some(outcome.get_type())
== CommunicationType::ErrorBadVersion.try_to_id(opening_codec.type_map())
{
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error(
outcome
.get_str(DataType::ErrorMessage)
.unwrap_or("host does not support this protocol version"),
));
}
let negotiated_version = match outcome.get_data(DataType::Version) {
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
.ok_or_else(|| js_error("host omitted a valid negotiated protocol version"))?,
_ => return Err(js_error("host omitted a valid negotiated protocol version")),
};
if negotiated_version != PROTOCOL_VERSION {
return Err(js_error(
"host selected a protocol version the client did not offer",
));
}
let codec = mtp_codec::registry::VersionedCodec::for_version(
mtp_codec::registry::Registry::builtin(),
negotiated_version,
)
.ok_or_else(|| js_error("host returned an unsupported negotiated protocol version"))?;
transport.set_type_map(codec.type_map());
let outcome = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&outcome_bytes,
codec.type_map(),
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse negotiated handshake outcome: {e}")))?;
let tm = codec.type_map();
let expected = CommunicationType::IdentificationResponse
.try_to_id(&tm)
.ok_or_else(|| js_error("IdentificationResponse is absent from the type map"))?;
if outcome.get_type() != expected
|| outcome.get_data(DataType::Connected) != Some(&DataValue::BoolTrue)
{
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error(
outcome
.get_str(DataType::ErrorMessage)
.unwrap_or("host rejected the connection"),
));
}
let assigned_id = match outcome.get_data(DataType::Id) {
Some(DataValue::UnsignedNumber(id)) => {
u64::try_from(*id).map_err(|_| js_error("assigned ID is out of range"))?
}
_ => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error("host omitted the assigned client ID"));
}
};
if !self.start_receive_loop(transport.clone(), generation, assigned_id) {
return Err(js_error("connection attempt superseded"));
}
Ok(())
}
.await;
if let Err(error) = &result {
self.abort_attempt(&transport, generation);
let _ = error;
}
result
}
#[wasm_bindgen]
#[deprecated(
note = "use the SDK authentication methods; this raw method remains for compatibility"
)]
pub async fn auth_connect(
&self,
config: &ConnectionConfig,
host_public_key_bytes: &[u8],
keyring_bytes: &[u8],
client_id: u64,
) -> Result<u64, JsValue> {
self.auth_connect_owned(
config.clone(),
host_public_key_bytes.to_vec(),
keyring_bytes.to_vec(),
client_id,
)
.await
}
#[wasm_bindgen(js_name = authConnectOwned)]
pub async fn auth_connect_owned(
&self,
config: ConnectionConfig,
host_public_key_bytes: Vec<u8>,
keyring_bytes: Vec<u8>,
client_id: u64,
) -> Result<u64, JsValue> {
let generation = self.begin_connection();
let host_pk = match mtp_crypto::PublicKeyBundle::from_bytes(&host_public_key_bytes) {
Ok(value) => value,
Err(error) => {
let error = js_error(format!("invalid host public key: {}", error));
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
let keyring = match mtp_crypto::Keyring::from_bytes(&keyring_bytes) {
Ok(value) => value,
Err(error) => {
let error = js_error(format!("invalid keyring: {}", error));
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
let handshake_codec = mtp_codec::registry::VersionedCodec::for_version(
mtp_codec::registry::Registry::builtin(),
PROTOCOL_VERSION,
)
.ok_or_else(|| js_error("client protocol version is not registered"))?;
let tm = handshake_codec.type_map().clone();
let version_str = format!("{}", PROTOCOL_VERSION);
let public_key_bytes = keyring
.public_key_bundle()
.try_as_bytes()
.map_err(|error| js_error(format!("public key serialization failed: {error}")))?;
let transport = match WasmTransport::connect_with_limits(
&config.url,
config.server_certificate_hashes.clone(),
config.max_message_size,
self.receive_decode_limits(),
)
.await
{
Ok(transport) => transport,
Err(error) => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
transport.set_type_map(&tm);
if !self.install_attempt_transport(&transport, generation) {
return Err(js_error("connection attempt superseded"));
}
let result = async {
let mut hello =
CommunicationValue::new_with_type_map(CommunicationType::Identification, &tm)
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128))
// Mark this as an authentication-capable opening so a
// non-crypto host can reject it explicitly.
.add_typed_default(
DataType::PublicKeys,
DataValue::Bytes(public_key_bytes.clone()),
);
if let Some(desc) = &config.description {
hello =
hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
}
let hello_bytes = hello
.to_bytes()
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
transport.send_frame(&hello_bytes).await?;
let server_challenge = self
.read_verified_challenge(
&transport,
&tm,
&host_pk,
client_id,
"auth_connect challenge",
config.require_pq,
!keyring.sig_pq_secret_key.as_bytes().is_empty(),
generation,
)
.await?;
let client_nonce = auth::random_nonce()?;
let proof_payload = mtp_crypto::auth::login_proof_payload(
&version_str,
client_id,
server_challenge,
client_nonce,
);
let proof =
auth::signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce, &tm)?;
transport.send_frame(&proof).await?;
let response = transport.read_one_frame().await?;
let resp_comm = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&response,
&tm,
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse response: {}", e)))?;
let negotiated_version = match resp_comm.get_data(DataType::Version) {
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
.ok_or_else(|| js_error("host returned an invalid negotiated version"))?,
_ => return Err(js_error("host omitted the negotiated version")),
};
if negotiated_version != PROTOCOL_VERSION {
return Err(js_error(
"host selected a protocol version the client did not offer",
));
}
let codec = mtp_codec::registry::VersionedCodec::for_version(
mtp_codec::registry::Registry::builtin(),
negotiated_version,
)
.ok_or_else(|| js_error("host returned an unsupported negotiated version"))?;
transport.set_type_map(codec.type_map());
let resp_comm = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&response,
codec.type_map(),
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse negotiated response: {}", e)))?;
let tm = codec.type_map();
let resp_type = resp_comm.get_type();
let expected_type = CommunicationType::IdentificationResponse
.try_to_id(&tm)
.ok_or_else(|| js_error("IdentificationResponse is absent from the type map"))?;
if resp_type != expected_type {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(auth::unexpected_response_type_error(
"auth_connect",
expected_type,
resp_type,
&response,
&resp_comm,
));
}
if resp_comm.get_data(DataType::Connected) != Some(&DataValue::BoolTrue) {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error(
resp_comm
.get_str(DataType::ErrorMessage)
.unwrap_or("host rejected authentication"),
));
}
if let Err(e) = auth::verify_host_final(
&resp_comm,
&tm,
&host_pk,
client_id,
client_nonce,
server_challenge,
config.require_pq,
) {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(e);
}
let assigned_id = match resp_comm.get_data(DataType::Id) {
Some(DataValue::UnsignedNumber(n)) => match u64::try_from(*n) {
Ok(id) => id,
Err(_) => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error("assigned ID is out of range"));
}
},
_ => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error("missing assigned ID"));
}
};
if !self.start_receive_loop(transport.clone(), generation, assigned_id) {
return Err(js_error("connection attempt superseded"));
}
Ok(assigned_id)
}
.await;
if let Err(error) = &result {
self.abort_attempt(&transport, generation);
let _ = error;
}
result
}
#[wasm_bindgen]
#[deprecated(
note = "use the SDK registration methods; this raw method remains for compatibility"
)]
pub async fn auth_register(
&self,
config: &ConnectionConfig,
host_public_key_bytes: &[u8],
keyring_bytes: &[u8],
) -> Result<u64, JsValue> {
self.auth_register_owned(
config.clone(),
host_public_key_bytes.to_vec(),
keyring_bytes.to_vec(),
)
.await
}
#[wasm_bindgen(js_name = authRegisterOwned)]
pub async fn auth_register_owned(
&self,
config: ConnectionConfig,
host_public_key_bytes: Vec<u8>,
keyring_bytes: Vec<u8>,
) -> Result<u64, JsValue> {
let generation = self.begin_connection();
let host_pk = match mtp_crypto::PublicKeyBundle::from_bytes(&host_public_key_bytes) {
Ok(value) => value,
Err(error) => {
let error = js_error(format!("invalid host public key: {}", error));
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
let keyring = match mtp_crypto::Keyring::from_bytes(&keyring_bytes) {
Ok(value) => value,
Err(error) => {
let error = js_error(format!("invalid keyring: {}", error));
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
let handshake_codec = mtp_codec::registry::VersionedCodec::for_version(
mtp_codec::registry::Registry::builtin(),
PROTOCOL_VERSION,
)
.ok_or_else(|| js_error("client protocol version is not registered"))?;
let tm = handshake_codec.type_map().clone();
let version_str = format!("{}", PROTOCOL_VERSION);
let pk_bytes = keyring
.public_key_bundle()
.try_as_bytes()
.map_err(|error| js_error(format!("public key serialization failed: {error}")))?;
let transport = match WasmTransport::connect_with_limits(
&config.url,
config.server_certificate_hashes.clone(),
config.max_message_size,
self.receive_decode_limits(),
)
.await
{
Ok(transport) => transport,
Err(error) => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(error);
}
};
transport.set_type_map(&tm);
if !self.install_attempt_transport(&transport, generation) {
return Err(js_error("connection attempt superseded"));
}
let result = async {
let mut hello = CommunicationValue::new_with_type_map(CommunicationType::Register, &tm)
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
if let Some(desc) = &config.description {
hello =
hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
}
let hello_bytes = hello
.to_bytes()
.map_err(|e| js_error(format!("encode failed: {}", e)))?;
transport.send_frame(&hello_bytes).await?;
let server_challenge = self
.read_verified_challenge(
&transport,
&tm,
&host_pk,
0,
"auth_register challenge",
config.require_pq,
!keyring.sig_pq_secret_key.as_bytes().is_empty(),
generation,
)
.await?;
let client_nonce = auth::random_nonce()?;
let proof_payload = mtp_crypto::auth::register_proof_payload(
&version_str,
&pk_bytes,
server_challenge,
client_nonce,
);
let proof =
auth::signed_challenge_response_bytes(&keyring, &proof_payload, client_nonce, &tm)?;
transport.send_frame(&proof).await?;
let response = transport.read_one_frame().await?;
let resp_comm = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&response,
&tm,
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse response: {}", e)))?;
let negotiated_version = match resp_comm.get_data(DataType::Version) {
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
.ok_or_else(|| js_error("host returned an invalid negotiated version"))?,
_ => return Err(js_error("host omitted the negotiated version")),
};
if negotiated_version != PROTOCOL_VERSION {
return Err(js_error(
"host selected a protocol version the client did not offer",
));
}
let codec = mtp_codec::registry::VersionedCodec::for_version(
mtp_codec::registry::Registry::builtin(),
negotiated_version,
)
.ok_or_else(|| js_error("host returned an unsupported negotiated version"))?;
transport.set_type_map(codec.type_map());
let resp_comm = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&response,
codec.type_map(),
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse negotiated response: {}", e)))?;
let tm = codec.type_map();
let resp_type = resp_comm.get_type();
let expected_type = CommunicationType::RegisterResponse
.try_to_id(&tm)
.ok_or_else(|| js_error("RegisterResponse is absent from the type map"))?;
if resp_type != expected_type {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(auth::unexpected_response_type_error(
"auth_register",
expected_type,
resp_type,
&response,
&resp_comm,
));
}
if resp_comm.get_data(DataType::Connected) != Some(&DataValue::BoolTrue) {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error(
resp_comm
.get_str(DataType::ErrorMessage)
.unwrap_or("host rejected registration"),
));
}
let assigned_id = match resp_comm.get_data(DataType::Id) {
Some(DataValue::UnsignedNumber(n)) => match u64::try_from(*n) {
Ok(id) => id,
Err(_) => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error("assigned ID is out of range"));
}
},
_ => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error("missing assigned ID"));
}
};
if let Err(e) = auth::verify_host_final(
&resp_comm,
&tm,
&host_pk,
assigned_id,
client_nonce,
server_challenge,
config.require_pq,
) {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(e);
}
if !self.start_receive_loop(transport.clone(), generation, assigned_id) {
return Err(js_error("connection attempt superseded"));
}
Ok(assigned_id)
}
.await;
if let Err(error) = &result {
self.abort_attempt(&transport, generation);
let _ = error;
}
result
}
async fn read_verified_challenge(
&self,
transport: &WasmTransport,
tm: &mtp_codec::TypeMap,
host_pk: &mtp_crypto::PublicKeyBundle,
bound_id: u64,
context: &str,
require_pq: bool,
client_has_pq_key: bool,
generation: u32,
) -> Result<u128, JsValue> {
let challenge_bytes = transport.read_one_frame().await?;
let challenge = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&challenge_bytes,
tm,
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse challenge: {}", e)))?;
let expected = CommunicationType::Challenge
.try_to_id(tm)
.ok_or_else(|| js_error("Challenge is absent from the type map"))?;
if challenge.get_type() != expected {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(auth::unexpected_response_type_error(
context,
expected,
challenge.get_type(),
&challenge_bytes,
&challenge,
));
}
let server_challenge = match challenge.get_data(DataType::ServerNonce) {
Some(DataValue::UnsignedNumber(n)) => *n,
_ => {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error("missing server challenge"));
}
};
if challenge.get_data(DataType::RequirePq) == Some(&DataValue::BoolTrue)
&& !client_has_pq_key
{
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(js_error(
"host requires post-quantum authentication but the client PQ key is absent",
));
}
if let Err(e) = auth::verify_host_challenge(
&challenge,
tm,
host_pk,
bound_id,
server_challenge,
require_pq,
) {
self.set_state_if_current(generation, ConnectionState::Disconnected);
return Err(e);
}
Ok(server_challenge)
}
}

View file

@ -0,0 +1,102 @@
use wasm_bindgen::prelude::*;
use crate::client::{ConnectionState, WasmClient};
use crate::client_pipe;
use crate::transport::WasmTransport;
use super::dispatch::set_shared_state;
#[wasm_bindgen]
impl WasmClient {
pub fn disconnect(&self) {
self.connection_generation
.set(self.connection_generation.get().wrapping_add(1));
self.stop_protocol_pings();
if let Some(t) = self.transport.borrow_mut().take() {
t.close();
}
if let Some(t) = self.attempt_transport.borrow_mut().take() {
t.close();
}
self.subscriptions.borrow_mut().clear();
self.reject_pending_requests("disconnected");
client_pipe::reject_pending_pipe_creations(&self.pending_pipe_creations, "disconnected");
self.expired_pipe_creations.borrow_mut().clear();
client_pipe::reject_pending_pipes(&self.pending_pipes, "disconnected");
self.connection_client_id.set(0);
self.set_state(ConnectionState::Disconnected);
}
pub(super) fn set_state(&self, new_state: ConnectionState) {
set_shared_state(
&self.state,
&self.pending_state_callbacks,
self.state_callback.as_ref(),
new_state,
);
}
pub(super) fn set_state_if_current(&self, generation: u32, new_state: ConnectionState) {
if self.connection_generation.get() == generation {
self.set_state(new_state);
}
}
pub(super) fn install_attempt_transport(
&self,
transport: &WasmTransport,
generation: u32,
) -> bool {
if self.connection_generation.get() != generation {
transport.close();
return false;
}
*self.attempt_transport.borrow_mut() = Some(transport.clone());
true
}
pub(super) fn abort_attempt(&self, transport: &WasmTransport, generation: u32) {
transport.close();
if self.connection_generation.get() != generation {
return;
}
if let Some(current) = self.attempt_transport.borrow_mut().take() {
current.close();
}
if let Some(current) = self.transport.borrow_mut().take() {
current.close();
}
self.stop_protocol_pings();
self.reject_pending_requests("connection failed");
client_pipe::reject_pending_pipe_creations(
&self.pending_pipe_creations,
"connection failed",
);
self.expired_pipe_creations.borrow_mut().clear();
client_pipe::reject_pending_pipes(&self.pending_pipes, "connection failed");
self.connection_client_id.set(0);
self.set_state(ConnectionState::Disconnected);
}
pub(super) fn begin_connection(&self) -> u32 {
let generation = self.connection_generation.get().wrapping_add(1);
self.connection_generation.set(generation);
self.stop_protocol_pings();
if let Some(transport) = self.transport.borrow_mut().take() {
transport.close();
}
if let Some(transport) = self.attempt_transport.borrow_mut().take() {
transport.close();
}
self.reject_pending_requests("connection replaced");
client_pipe::reject_pending_pipe_creations(
&self.pending_pipe_creations,
"connection replaced",
);
self.expired_pipe_creations.borrow_mut().clear();
client_pipe::reject_pending_pipes(&self.pending_pipes, "connection replaced");
self.connection_client_id.set(0);
self.set_state(ConnectionState::Connecting);
generation
}
}

184
wasm/src/client/dispatch.rs Normal file
View file

@ -0,0 +1,184 @@
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use crate::client::ConnectionState;
use crate::client_pipe::{self, PendingRequest};
pub(super) struct PingTimer {
pub(super) id: i32,
pub(super) closure: Closure<dyn FnMut()>,
}
pub(super) struct PendingPing {
pub(super) generation: u32,
pub(super) sent_at: f64,
}
pub(super) fn frame_property(frame: &JsValue, key: &str) -> Option<JsValue> {
js_sys::Reflect::get(frame, &JsValue::from_str(key))
.ok()
.filter(|value| !value.is_null() && !value.is_undefined())
}
pub(super) fn frame_id(frame: &JsValue) -> Option<u32> {
frame_property(frame, "id")
.and_then(|value| value.as_f64())
.filter(|value| {
value.is_finite() && value.fract() == 0.0 && (0.0..=u32::MAX as f64).contains(value)
})
.and_then(|value| u32::try_from(value as u64).ok())
}
pub(super) fn frame_type(frame: &JsValue) -> Option<String> {
frame_property(frame, "type").and_then(|value| value.as_string())
}
pub(super) fn route_incoming_frame(
frame: &JsValue,
generation: u32,
on_message: &js_sys::Function,
subscriptions: &Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
expired_requests: &Rc<RefCell<HashMap<u32, f64>>>,
pending_pings: &Rc<RefCell<HashMap<u32, PendingPing>>>,
ping_ms: &Rc<Cell<Option<f64>>>,
) {
let message_type = frame_type(frame);
if message_type.as_deref() == Some("Pong")
&& let Some(ping_id) = frame_id(frame)
{
let sent_at = pending_pings
.borrow()
.get(&ping_id)
.filter(|ping| ping.generation == generation)
.map(|ping| ping.sent_at);
if let Some(sent_at) = sent_at {
pending_pings.borrow_mut().remove(&ping_id);
ping_ms.set(Some(js_sys::Date::now() - sent_at));
return;
}
}
if let Some(request_id) = frame_id(frame) {
let pending = {
let mut requests = pending_requests.borrow_mut();
if requests
.get(&request_id)
.is_some_and(|request| request.generation == generation)
{
requests.remove(&request_id)
} else {
None
}
};
if let Some(pending) = pending {
let type_matches = pending
.response_type
.as_ref()
.zip(message_type.as_ref())
.map(|(expected, actual)| expected == actual)
.unwrap_or(true);
if type_matches {
let _ = pending.sender.send(Ok(frame.clone()));
} else {
let actual = message_type.clone().unwrap_or_else(|| "unknown".into());
let _ = pending.sender.send(Err(crate::error::js_error(format!(
"unexpected response type: expected {}, got {}",
pending.response_type.unwrap_or_else(|| "unknown".into()),
actual
))));
}
return;
}
if client_pipe::consume_expired_request(expired_requests, request_id) {
return;
}
}
let _ = on_message.call1(&JsValue::NULL, frame);
let Some(message_type) = message_type else {
return;
};
let callbacks: Vec<js_sys::Function> = subscriptions
.borrow()
.iter()
.filter(|(_, (t, _))| t == &message_type)
.map(|(_, (_, cb))| cb.clone())
.collect();
for callback in callbacks {
let _ = callback.call1(&JsValue::NULL, frame);
}
}
pub(super) fn stop_ping_timer(ping_timer: &Rc<RefCell<Option<PingTimer>>>) {
let Some(timer) = ping_timer.borrow_mut().take() else {
return;
};
if let Ok(clear_interval) =
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
.and_then(|value| value.dyn_into::<js_sys::Function>())
{
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
}
drop(timer.closure);
}
pub(super) fn reject_pending_requests(
pending_requests: &Rc<RefCell<HashMap<u32, PendingRequest>>>,
message: &str,
) {
let pending = std::mem::take(&mut *pending_requests.borrow_mut());
for (_, pending) in pending {
let _ = pending.sender.send(Err(crate::error::js_error(message)));
}
}
pub(super) async fn wait_for_timeout(timeout_ms: u32) -> Result<(), JsValue> {
let promise = js_sys::Promise::new(&mut |resolve, reject| {
let result = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
.and_then(|value| value.dyn_into::<js_sys::Function>())
.and_then(|set_timeout| {
set_timeout.call2(
&JsValue::NULL,
&resolve,
&JsValue::from_f64(timeout_ms as f64),
)
});
if let Err(error) = result {
let _ = reject.call1(&JsValue::NULL, &error);
}
});
wasm_bindgen_futures::JsFuture::from(promise).await?;
Ok(())
}
pub(super) fn set_shared_state(
state: &Rc<Cell<ConnectionState>>,
pending_state_callbacks: &Rc<RefCell<VecDeque<ConnectionState>>>,
state_callback: &JsValue,
new_state: ConnectionState,
) {
state.set(new_state);
pending_state_callbacks.borrow_mut().push_back(new_state);
let global = js_sys::global();
let qmt = js_sys::Reflect::get(&global, &JsValue::from_str("queueMicrotask"))
.and_then(|f| f.dyn_into::<js_sys::Function>());
let scheduled = qmt
.and_then(|qmt| qmt.call1(&global, state_callback))
.is_ok();
if !scheduled
&& js_sys::Reflect::get(&global, &JsValue::from_str("setTimeout"))
.and_then(|f| f.dyn_into::<js_sys::Function>())
.and_then(|set_timeout| {
set_timeout.call2(&global, state_callback, &JsValue::from_f64(0.0))
})
.is_err()
{
pending_state_callbacks.borrow_mut().pop_back();
}
}

299
wasm/src/client/mod.rs Normal file
View file

@ -0,0 +1,299 @@
// WASM client facade. Lifecycle, authentication, receive dispatch, and pipes
// live in private child modules below.
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::collections::VecDeque;
use std::rc::Rc;
use futures_channel::oneshot;
use futures_util::{FutureExt, pin_mut, select};
use wasm_bindgen::prelude::*;
use mtp_codec::{CommunicationValue, DecodeLimits, EncodeLimits};
use crate::client_pipe::{self, PendingRequest};
use crate::error::js_error;
use crate::transport::WasmTransport;
mod authentication;
mod connection;
mod dispatch;
mod pipes;
mod receive;
use dispatch::{PendingPing, PingTimer, wait_for_timeout};
const DEFAULT_REQUEST_TIMEOUT_MS: u32 = 30_000;
const MAX_SAFE_JS_INTEGER: f64 = 9_007_199_254_740_991.0;
fn decode_limit(value: &JsValue, key: &str, default: usize) -> Result<usize, JsValue> {
if value.is_null() || value.is_undefined() {
return Ok(default);
}
let value = js_sys::Reflect::get(value, &JsValue::from_str(key))?;
if value.is_null() || value.is_undefined() {
return Ok(default);
}
let Some(number) = value.as_f64() else {
return Err(js_error(format!("{key} must be a number")));
};
if !number.is_finite() || number.fract() != 0.0 || number < 0.0 || number > MAX_SAFE_JS_INTEGER
{
return Err(js_error(format!("{key} must be a non-negative integer")));
}
usize::try_from(number as u64).map_err(|_| js_error(format!("{key} is out of range")))
}
pub(crate) fn encode_limits_from_js(value: &JsValue) -> Result<EncodeLimits, JsValue> {
let defaults = EncodeLimits::default();
Ok(EncodeLimits {
max_depth: decode_limit(value, "maxDepth", defaults.max_depth)?,
max_values: decode_limit(value, "maxValues", defaults.max_values)?,
max_output_size: decode_limit(value, "maxOutputSize", defaults.max_output_size)?,
})
}
pub(crate) fn decode_limits_from_js(value: &JsValue) -> Result<DecodeLimits, JsValue> {
let defaults = DecodeLimits::default();
Ok(DecodeLimits {
max_depth: decode_limit(value, "maxDepth", defaults.max_depth)?,
max_values: decode_limit(value, "maxValues", defaults.max_values)?,
max_blob_size: decode_limit(value, "maxBlobSize", defaults.max_blob_size)?,
max_recipients: decode_limit(value, "maxRecipients", defaults.max_recipients)?,
max_allocated_bytes: decode_limit(
value,
"maxAllocatedBytes",
defaults.max_allocated_bytes,
)?,
})
}
#[wasm_bindgen]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
Disconnected = 0,
Connecting = 1,
Connected = 2,
Failed = 3,
}
#[wasm_bindgen]
pub struct WasmClient {
transport: Rc<RefCell<Option<WasmTransport>>>,
attempt_transport: Rc<RefCell<Option<WasmTransport>>>,
connection_generation: Rc<Cell<u32>>,
state: Rc<Cell<ConnectionState>>,
pending_state_callbacks: Rc<RefCell<VecDeque<ConnectionState>>>,
state_callback: Closure<dyn FnMut()>,
pub(crate) on_message: js_sys::Function,
pub(crate) on_error: js_sys::Function,
subscriptions: Rc<RefCell<HashMap<u32, (String, js_sys::Function)>>>,
next_subscription_id: Rc<Cell<u32>>,
pending_requests: Rc<RefCell<HashMap<u32, PendingRequest>>>,
expired_requests: Rc<RefCell<HashMap<u32, f64>>>,
ping_timer: Rc<RefCell<Option<PingTimer>>>,
pending_pings: Rc<RefCell<HashMap<u32, PendingPing>>>,
ping_ms: Rc<Cell<Option<f64>>>,
pending_pipe_creations: client_pipe::PendingPipeCreations,
expired_pipe_creations: Rc<RefCell<HashMap<u32, f64>>>,
pending_pipes: client_pipe::PendingPipes,
connection_client_id: Rc<Cell<u64>>,
on_pipe_request: Rc<RefCell<Option<js_sys::Function>>>,
receive_decode_limits: Rc<RefCell<Option<DecodeLimits>>>,
}
#[wasm_bindgen]
#[allow(deprecated)]
impl WasmClient {
#[wasm_bindgen(constructor)]
pub fn new(
on_state_change: Option<js_sys::Function>,
on_message: Option<js_sys::Function>,
on_error: Option<js_sys::Function>,
) -> Self {
let noop = || js_sys::Function::new_no_args("");
let on_state_change = on_state_change.unwrap_or_else(noop);
let pending_state_callbacks = Rc::new(RefCell::new(VecDeque::new()));
let callback_queue = pending_state_callbacks.clone();
let callback = on_state_change.clone();
let state_callback = Closure::wrap(Box::new(move || {
let state = callback_queue.borrow_mut().pop_front();
if let Some(state) = state {
let _ = callback.call1(&JsValue::NULL, &JsValue::from(state as u8));
}
}) as Box<dyn FnMut()>);
Self {
transport: Rc::new(RefCell::new(None)),
attempt_transport: Rc::new(RefCell::new(None)),
connection_generation: Rc::new(Cell::new(0)),
state: Rc::new(Cell::new(ConnectionState::Disconnected)),
pending_state_callbacks,
state_callback,
on_message: on_message.unwrap_or_else(noop),
on_error: on_error.unwrap_or_else(noop),
subscriptions: Rc::new(RefCell::new(HashMap::new())),
next_subscription_id: Rc::new(Cell::new(1)),
pending_requests: Rc::new(RefCell::new(HashMap::new())),
expired_requests: Rc::new(RefCell::new(HashMap::new())),
ping_timer: Rc::new(RefCell::new(None)),
pending_pings: Rc::new(RefCell::new(HashMap::new())),
ping_ms: Rc::new(Cell::new(None)),
pending_pipe_creations: Rc::new(RefCell::new(HashMap::new())),
expired_pipe_creations: Rc::new(RefCell::new(HashMap::new())),
pending_pipes: Rc::new(RefCell::new(HashMap::new())),
connection_client_id: Rc::new(Cell::new(0)),
on_pipe_request: Rc::new(RefCell::new(None)),
receive_decode_limits: Rc::new(RefCell::new(None)),
}
}
pub fn is_supported() -> bool {
js_sys::Reflect::has(&js_sys::global(), &JsValue::from_str("WebTransport")).unwrap_or(false)
}
#[wasm_bindgen(getter)]
pub fn state(&self) -> u8 {
self.state.get() as u8
}
#[wasm_bindgen(getter)]
pub fn ping_ms(&self) -> Option<f64> {
self.ping_ms.get()
}
#[wasm_bindgen(getter)]
pub fn client_id(&self) -> u64 {
self.connection_client_id.get()
}
/// Apply one decoder policy to frames received by this raw WASM client.
/// The high-level SDK calls this before authentication so handshake,
/// transport, and protected opening share the same policy input.
#[wasm_bindgen]
pub fn set_receive_limits(&self, limits: JsValue) -> Result<(), JsValue> {
let parsed = if limits.is_null() || limits.is_undefined() {
None
} else {
Some(decode_limits_from_js(&limits)?)
};
*self.receive_decode_limits.borrow_mut() = parsed;
Ok(())
}
pub(super) fn receive_decode_limits(&self) -> Option<DecodeLimits> {
*self.receive_decode_limits.borrow()
}
#[wasm_bindgen]
#[deprecated(
note = "use the SDK connection methods; this raw method remains for compatibility"
)]
pub async fn send(&self, frame: Vec<u8>) -> Result<(), JsValue> {
if self.state.get() != ConnectionState::Connected {
return Err(js_error("not connected"));
}
let transport = self.transport.borrow().clone();
match transport {
Some(t) => t.send_frame(&frame).await,
None => Err(js_error("not connected")),
}
}
#[wasm_bindgen]
pub async fn request(
&self,
frame: Vec<u8>,
response_type: Option<String>,
timeout_ms: Option<u32>,
) -> Result<JsValue, JsValue> {
if self.state.get() != ConnectionState::Connected {
return Err(js_error("not connected"));
}
let generation = self.connection_generation.get();
let Some(transport) = self.transport.borrow().clone() else {
return Err(js_error("not connected"));
};
let request = CommunicationValue::try_from_bytes_with_type_map_and_limits(
&frame,
&transport.type_map(),
transport.decode_limits(),
)
.map_err(|e| js_error(format!("parse request: {}", e)))?;
let request_id = request
.id()
.ok_or_else(|| js_error("request frame must contain an id"))?;
if request_id == 0 {
return Err(js_error("request frame must have a non-zero id"));
}
if client_pipe::is_expired_request(&self.expired_requests, request_id) {
return Err(js_error(format!(
"request id {request_id} recently timed out; use a new request id"
)));
}
let (sender, receiver) = oneshot::channel();
let token = Rc::new(());
{
let mut pending = self.pending_requests.borrow_mut();
if pending.contains_key(&request_id) {
return Err(js_error(format!(
"request id {request_id} is already pending"
)));
}
pending.insert(
request_id,
PendingRequest {
generation,
token: token.clone(),
response_type,
sender,
},
);
}
let timeout_ms = timeout_ms.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS);
let response = async {
transport.send_frame(&frame).await?;
match receiver.await {
Ok(result) => result,
Err(_) => Err(js_error("request cancelled")),
}
}
.fuse();
let timeout = wait_for_timeout(timeout_ms).fuse();
pin_mut!(response, timeout);
select! {
result = response => {
if result.is_err() {
client_pipe::remove_pending_request(&self.pending_requests, request_id, &token);
}
result
},
result = timeout => {
client_pipe::expire_pending_request(
&self.pending_requests,
&self.expired_requests,
request_id,
&token,
);
result?;
Err(js_error(format!(
"request {request_id} timed out after {timeout_ms}ms"
)))
},
}
}
#[wasm_bindgen]
pub fn subscribe(&self, message_type: String, callback: js_sys::Function) -> u32 {
let id = self.next_subscription_id.get();
self.next_subscription_id.set(id.wrapping_add(1).max(1));
self.subscriptions
.borrow_mut()
.insert(id, (message_type, callback));
id
}
#[wasm_bindgen]
pub fn unsubscribe(&self, id: u32) -> bool {
self.subscriptions.borrow_mut().remove(&id).is_some()
}
}

76
wasm/src/client/pipes.rs Normal file
View file

@ -0,0 +1,76 @@
use wasm_bindgen::prelude::*;
use crate::client::{ConnectionState, WasmClient};
use crate::client_pipe;
use crate::error::js_error;
use crate::pipe::PipeReader;
#[wasm_bindgen]
impl WasmClient {
pub fn set_on_pipe_request(&self, callback: Option<js_sys::Function>) {
*self.on_pipe_request.borrow_mut() = callback;
}
#[wasm_bindgen]
pub async fn create_pipe(
&self,
description: &str,
) -> Result<client_pipe::WasmPipeHandle, JsValue> {
if self.state.get() != ConnectionState::Connected {
return Err(js_error("not connected"));
}
let transport = self
.transport
.borrow()
.clone()
.ok_or_else(|| js_error("not connected"))?;
let pipe_id = client_pipe::random_pipe_id()?;
client_pipe::wasm_create_pipe(
&transport,
description,
pipe_id,
&self.pending_pipe_creations,
&self.expired_pipe_creations,
self.connection_generation.get(),
&self.connection_generation,
)
.await
}
#[wasm_bindgen]
pub async fn accept_pipe(&self, pipe_id: u32) -> Result<PipeReader, JsValue> {
if self.state.get() != ConnectionState::Connected {
return Err(js_error("not connected"));
}
let transport = self
.transport
.borrow()
.clone()
.ok_or_else(|| js_error("not connected"))?;
let generation = self.connection_generation.get();
client_pipe::wasm_accept_pipe(
&transport,
pipe_id,
&self.pending_pipes,
generation,
&self.connection_generation,
)
.await
}
#[wasm_bindgen]
pub async fn deny_pipe(&self, pipe_id: u32) -> Result<(), JsValue> {
if self.state.get() != ConnectionState::Connected {
return Err(js_error("not connected"));
}
let transport = self
.transport
.borrow()
.clone()
.ok_or_else(|| js_error("not connected"))?;
client_pipe::wasm_deny_pipe(&transport, pipe_id).await
}
}

328
wasm/src/client/receive.rs Normal file
View file

@ -0,0 +1,328 @@
use wasm_bindgen::prelude::*;
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use crate::client::{ConnectionState, WasmClient};
use crate::client_pipe;
use crate::error::js_error;
use crate::pipe::PipeReader;
use crate::transport::WasmTransport;
use super::MAX_SAFE_JS_INTEGER;
use super::dispatch::{
PendingPing, PingTimer, frame_id, frame_property, frame_type, reject_pending_requests,
route_incoming_frame, set_shared_state, stop_ping_timer,
};
#[wasm_bindgen]
impl WasmClient {
pub fn start_protocol_pings(&self, interval_ms: u32, client_id: u64) -> Result<(), JsValue> {
self.stop_protocol_pings();
if self.state.get() != ConnectionState::Connected {
return Err(js_error("not connected"));
}
let Some(transport) = self.transport.borrow().clone() else {
return Err(js_error("not connected"));
};
let generation = self.connection_generation.get();
let current_generation = self.connection_generation.clone();
let interval_ms = i32::try_from(interval_ms.max(1_000))
.map_err(|_| js_error("ping interval is too large"))?;
let on_error = self.on_error.clone();
let pending_pings = self.pending_pings.clone();
let closure = Closure::wrap(Box::new(move || {
if current_generation.get() != generation {
return;
}
let transport = transport.clone();
let on_error = on_error.clone();
let pending_pings = pending_pings.clone();
let current_generation = current_generation.clone();
wasm_bindgen_futures::spawn_local(async move {
if current_generation.get() != generation {
return;
}
let sent_at = js_sys::Date::now();
pending_pings.borrow_mut().retain(|_, pending| {
pending.generation == generation
&& sent_at - pending.sent_at < interval_ms as f64 * 3.0
});
let timestamp = if sent_at.is_finite()
&& sent_at >= 0.0
&& sent_at <= MAX_SAFE_JS_INTEGER
&& sent_at.fract() == 0.0
{
sent_at as u64
} else {
let _ = on_error.call1(&JsValue::NULL, &js_error("invalid clock value"));
return;
};
let type_map = transport.type_map();
let frame =
CommunicationValue::new_with_type_map(CommunicationType::Ping, &type_map)
.add_typed_default(
DataType::Description,
DataValue::Str("protocol ping".into()),
)
.add_typed_default(
DataType::Timestamp,
DataValue::UnsignedNumber(timestamp as u128),
)
.with_sender(client_id);
let Some(ping_id) = frame.id() else {
let _ = on_error.call1(&JsValue::NULL, &js_error("ping frame has no id"));
return;
};
let frame = frame
.to_bytes()
.map_err(|e| js_error(format!("encode ping failed: {}", e)));
match frame {
Ok(frame) => {
if current_generation.get() != generation {
return;
}
pending_pings.borrow_mut().insert(
ping_id,
PendingPing {
generation,
sent_at,
},
);
if let Err(error) = transport.send_frame(&frame).await {
if pending_pings
.borrow()
.get(&ping_id)
.is_some_and(|ping| ping.generation == generation)
{
pending_pings.borrow_mut().remove(&ping_id);
}
let _ = on_error.call1(&JsValue::NULL, &error);
}
}
Err(error) => {
let _ = on_error.call1(&JsValue::NULL, &error);
}
}
});
}) as Box<dyn FnMut()>);
let set_interval =
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setInterval"))?
.dyn_into::<js_sys::Function>()?;
let id = set_interval
.call2(
&JsValue::NULL,
closure.as_ref().unchecked_ref(),
&JsValue::from_f64(interval_ms as f64),
)?
.as_f64()
.filter(|value| {
value.is_finite()
&& value.fract() == 0.0
&& (i32::MIN as f64..=i32::MAX as f64).contains(value)
})
.and_then(|value| i32::try_from(value as i64).ok())
.ok_or_else(|| js_error("setInterval did not return a valid id"))?;
*self.ping_timer.borrow_mut() = Some(PingTimer { id, closure });
Ok(())
}
#[wasm_bindgen]
pub fn stop_protocol_pings(&self) {
self.pending_pings.borrow_mut().clear();
self.ping_ms.set(None);
let Some(timer) = self.ping_timer.borrow_mut().take() else {
return;
};
if let Ok(clear_interval) =
js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("clearInterval"))
.and_then(|value| value.dyn_into::<js_sys::Function>())
{
let _ = clear_interval.call1(&JsValue::NULL, &JsValue::from_f64(timer.id as f64));
}
drop(timer.closure);
}
pub(super) fn start_receive_loop(
&self,
transport: WasmTransport,
generation: u32,
client_id: u64,
) -> bool {
if self.connection_generation.get() != generation {
transport.close();
return false;
}
let loop_transport = transport.clone();
self.attempt_transport.borrow_mut().take();
*self.transport.borrow_mut() = Some(transport);
self.set_state(ConnectionState::Connected);
let connection_generation = self.connection_generation.clone();
let error_generation = connection_generation.clone();
let state = self.state.clone();
let pending_state_callbacks = self.pending_state_callbacks.clone();
let state_callback = self.state_callback.as_ref().clone();
let on_msg = self.on_message.clone();
let on_err = self.on_error.clone();
let subscriptions = self.subscriptions.clone();
let pending_requests = self.pending_requests.clone();
let loop_pending_requests = pending_requests.clone();
let expired_requests = self.expired_requests.clone();
let loop_expired_requests = expired_requests.clone();
let ping_timer = self.ping_timer.clone();
let pending_pings = self.pending_pings.clone();
let loop_pending_pings = pending_pings.clone();
let ping_ms = self.ping_ms.clone();
let loop_ping_ms = ping_ms.clone();
let pending_pipe_creations = self.pending_pipe_creations.clone();
let expired_pipe_creations = self.expired_pipe_creations.clone();
let pending_pipes = self.pending_pipes.clone();
let loop_pending_pipes = pending_pipes.clone();
let on_pipe_request = self.on_pipe_request.clone();
let loop_pipe_creations = pending_pipe_creations.clone();
let loop_expired_pipe_creations = expired_pipe_creations.clone();
let loop_generation = generation;
let frame_generation = connection_generation.clone();
let transport_for_cleanup = self.transport.clone();
let connection_client_id = self.connection_client_id.clone();
wasm_bindgen_futures::spawn_local(async move {
loop_transport
.receive_loop_with_pipes(
move |frame: JsValue| {
if frame_generation.get() != loop_generation {
return;
}
let message_type = frame_type(&frame);
if let Some(ref msg_type) = message_type {
if msg_type == "PipeRequest" {
let Some(pipe_id) = frame_id(&frame).filter(|id| *id != 0) else {
return;
};
let description = frame_property(&frame, "data")
.and_then(|data| {
let desc = js_sys::Reflect::get(
&data,
&JsValue::from_str("Description"),
)
.ok()?;
desc.as_string()
})
.unwrap_or_default();
let cb = on_pipe_request.borrow();
if let Some(ref callback) = *cb {
let obj = js_sys::Object::new();
let _ = js_sys::Reflect::set(
&obj,
&"pipeId".into(),
&JsValue::from_f64(pipe_id as f64),
);
let _ = js_sys::Reflect::set(
&obj,
&"description".into(),
&JsValue::from_str(&description),
);
let _ = callback.call1(&JsValue::NULL, &obj.into());
}
return;
}
if msg_type == "PipeResponse" {
let Some(pipe_id) = frame_id(&frame).filter(|id| *id != 0) else {
return;
};
let accepted = frame_property(&frame, "data")
.and_then(|data| {
let acc = js_sys::Reflect::get(
&data,
&JsValue::from_str("Accepted"),
)
.ok()?;
acc.as_bool()
})
.unwrap_or(false);
let pending = {
let mut pending = loop_pipe_creations.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == loop_generation)
{
pending.remove(&pipe_id)
} else {
None
}
};
if let Some(entry) = pending {
let _ = entry.sender.send(Ok(accepted));
} else {
let _ = client_pipe::consume_expired_pipe_creation(
&loop_expired_pipe_creations,
pipe_id,
);
}
return;
}
}
route_incoming_frame(
&frame,
loop_generation,
&on_msg,
&subscriptions,
&loop_pending_requests,
&loop_expired_requests,
&loop_pending_pings,
&loop_ping_ms,
);
},
move |error| {
if error_generation.get() == generation {
let _ = on_err.call1(&JsValue::NULL, &error);
}
},
move |pipe_reader: PipeReader| {
let pipe_id = pipe_reader.pipe_id();
let mut pending = loop_pending_pipes.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == loop_generation)
&& let Some(entry) = pending.remove(&pipe_id)
{
let _ = entry.sender.send(Ok(pipe_reader));
}
},
)
.await;
if connection_generation.get() != generation {
return;
}
if let Some(current_transport) = transport_for_cleanup.borrow_mut().take() {
current_transport.close();
}
set_shared_state(
&state,
&pending_state_callbacks,
&state_callback,
ConnectionState::Disconnected,
);
stop_ping_timer(&ping_timer);
pending_pings.borrow_mut().clear();
ping_ms.set(None);
reject_pending_requests(&pending_requests, "disconnected");
expired_requests.borrow_mut().clear();
client_pipe::reject_pending_pipe_creations(&pending_pipe_creations, "disconnected");
expired_pipe_creations.borrow_mut().clear();
client_pipe::reject_pending_pipes(&pending_pipes, "disconnected");
connection_client_id.set(0);
});
self.connection_client_id.set(client_id);
true
}
pub(super) fn reject_pending_requests(&self, message: &str) {
reject_pending_requests(&self.pending_requests, message);
self.expired_requests.borrow_mut().clear();
}
}

View file

@ -3,8 +3,10 @@ use std::collections::HashMap;
use std::rc::Rc;
use futures_channel::oneshot;
use futures_util::{FutureExt, pin_mut, select};
use tracing::debug;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue};
@ -21,12 +23,17 @@ pub(crate) struct PendingRequest {
pub(crate) struct PendingPipeCreation {
pub(crate) generation: u32,
pub(crate) token: Rc<()>,
pub(crate) sender: oneshot::Sender<Result<bool, JsValue>>,
}
pub(crate) type PendingPipeCreations = Rc<RefCell<HashMap<u32, PendingPipeCreation>>>;
type PipeResponseReceiver = oneshot::Receiver<Result<bool, JsValue>>;
type PipeResponseCell = Rc<RefCell<Option<PipeResponseReceiver>>>;
const DEFAULT_PIPE_CREATION_TIMEOUT_MS: u32 = 30_000;
const EXPIRED_PIPE_CREATION_TOMBSTONE_TTL_MS: f64 = 60_000.0;
const MAX_EXPIRED_PIPE_CREATION_TOMBSTONES: usize = 1024;
pub(crate) struct PendingPipe {
pub(crate) generation: u32,
pub(crate) sender: oneshot::Sender<Result<PipeReader, JsValue>>,
@ -114,6 +121,10 @@ pub struct WasmPipeHandle {
description: String,
transport: WasmTransport,
response_rx: PipeResponseCell,
pending: PendingPipeCreations,
expired: Rc<RefCell<HashMap<u32, f64>>>,
generation: u32,
token: Rc<()>,
}
#[wasm_bindgen]
@ -125,9 +136,37 @@ impl WasmPipeHandle {
.take()
.ok_or_else(|| js_error("handle already consumed"))?;
let accepted = rx
.await
.map_err(|_| js_error("pipe handle channel closed"))?;
let response = rx.fuse();
let timeout = wait_for_timeout(DEFAULT_PIPE_CREATION_TIMEOUT_MS).fuse();
pin_mut!(response, timeout);
let accepted = select! {
result = response => match result {
Ok(result) => result,
Err(_) => {
expire_pending_pipe_creation(
&self.pending,
&self.expired,
self.pipe_id,
self.generation,
&self.token,
);
return Err(js_error("pipe handle channel closed"));
}
},
result = timeout => {
result?;
expire_pending_pipe_creation(
&self.pending,
&self.expired,
self.pipe_id,
self.generation,
&self.token,
);
return Err(js_error(format!(
"pipe creation timed out after {DEFAULT_PIPE_CREATION_TIMEOUT_MS}ms"
)));
},
};
match accepted {
Ok(true) => {
@ -153,6 +192,18 @@ impl WasmPipeHandle {
}
}
impl Drop for WasmPipeHandle {
fn drop(&mut self) {
expire_pending_pipe_creation(
&self.pending,
&self.expired,
self.pipe_id,
self.generation,
&self.token,
);
}
}
pub(crate) fn random_pipe_id() -> Result<u32, JsValue> {
let mut bytes = [0u8; 4];
getrandom_v04::fill(&mut bytes).map_err(|_| js_error("rng failed"))?;
@ -166,6 +217,146 @@ pub(crate) fn reject_pending_pipe_creations(pending: &PendingPipeCreations, mess
}
}
async fn wait_for_timeout(timeout_ms: u32) -> Result<(), JsValue> {
let promise = js_sys::Promise::new(&mut |resolve, reject| {
let result = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("setTimeout"))
.and_then(|value| value.dyn_into::<js_sys::Function>())
.and_then(|set_timeout| {
set_timeout.call2(
&JsValue::NULL,
&resolve,
&JsValue::from_f64(timeout_ms as f64),
)
});
if let Err(error) = result {
let _ = reject.call1(&JsValue::NULL, &error);
}
});
JsFuture::from(promise).await?;
Ok(())
}
fn expire_pending_pipe_creation(
pending: &PendingPipeCreations,
expired: &Rc<RefCell<HashMap<u32, f64>>>,
pipe_id: u32,
generation: u32,
token: &Rc<()>,
) {
let removed = {
let mut pending = pending.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == generation && Rc::ptr_eq(&entry.token, token))
{
pending.remove(&pipe_id);
true
} else {
false
}
};
if !removed {
return;
}
let now = js_sys::Date::now();
let mut expired = expired.borrow_mut();
expired.retain(|_, expires_at| *expires_at > now);
if expired.len() >= MAX_EXPIRED_PIPE_CREATION_TOMBSTONES
&& let Some(oldest) = expired
.iter()
.min_by(|(_, left), (_, right)| left.total_cmp(right))
.map(|(id, _)| *id)
{
expired.remove(&oldest);
}
expired.insert(pipe_id, now + EXPIRED_PIPE_CREATION_TOMBSTONE_TTL_MS);
}
struct PendingPipeCreationGuard {
pending: PendingPipeCreations,
expired: Rc<RefCell<HashMap<u32, f64>>>,
pipe_id: u32,
generation: u32,
token: Rc<()>,
armed: bool,
}
impl PendingPipeCreationGuard {
fn new(
pending: PendingPipeCreations,
expired: Rc<RefCell<HashMap<u32, f64>>>,
pipe_id: u32,
generation: u32,
token: Rc<()>,
) -> Self {
Self {
pending,
expired,
pipe_id,
generation,
token,
armed: true,
}
}
fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for PendingPipeCreationGuard {
fn drop(&mut self) {
if self.armed {
expire_pending_pipe_creation(
&self.pending,
&self.expired,
self.pipe_id,
self.generation,
&self.token,
);
}
}
}
struct PendingPipeGuard {
pending: PendingPipes,
pipe_id: u32,
generation: u32,
}
impl PendingPipeGuard {
fn new(pending: PendingPipes, pipe_id: u32, generation: u32) -> Self {
Self {
pending,
pipe_id,
generation,
}
}
}
impl Drop for PendingPipeGuard {
fn drop(&mut self) {
remove_pending_pipe(&self.pending, self.pipe_id, self.generation);
}
}
pub(crate) fn consume_expired_pipe_creation(
expired: &Rc<RefCell<HashMap<u32, f64>>>,
pipe_id: u32,
) -> bool {
let now = js_sys::Date::now();
let mut expired = expired.borrow_mut();
expired.retain(|_, expires_at| *expires_at > now);
expired.remove(&pipe_id).is_some()
}
fn is_expired_pipe_creation(expired: &Rc<RefCell<HashMap<u32, f64>>>, pipe_id: u32) -> bool {
let now = js_sys::Date::now();
let mut expired = expired.borrow_mut();
expired.retain(|_, expires_at| *expires_at > now);
expired.contains_key(&pipe_id)
}
pub(crate) fn reject_pending_pipes(pending: &PendingPipes, message: &str) {
let pending = std::mem::take(&mut *pending.borrow_mut());
for (_, entry) in pending {
@ -178,19 +369,26 @@ pub(crate) async fn wasm_create_pipe(
description: &str,
pipe_id: u32,
pending_pipe_creations: &PendingPipeCreations,
expired_pipe_creations: &Rc<RefCell<HashMap<u32, f64>>>,
generation: u32,
current_generation: &Rc<std::cell::Cell<u32>>,
) -> Result<WasmPipeHandle, JsValue> {
let (tx, rx) = oneshot::channel();
let token = Rc::new(());
let mut pipe_id = pipe_id;
for _ in 0..128 {
let occupied = pipe_id == 0 || pending_pipe_creations.borrow().contains_key(&pipe_id);
let occupied = pipe_id == 0
|| pending_pipe_creations.borrow().contains_key(&pipe_id)
|| is_expired_pipe_creation(expired_pipe_creations, pipe_id);
if !occupied {
break;
}
pipe_id = random_pipe_id()?;
}
if pipe_id == 0 || pending_pipe_creations.borrow().contains_key(&pipe_id) {
if pipe_id == 0
|| pending_pipe_creations.borrow().contains_key(&pipe_id)
|| is_expired_pipe_creation(expired_pipe_creations, pipe_id)
{
return Err(js_error("could not allocate a unique pipe id"));
}
let type_map = transport.type_map();
@ -207,9 +405,17 @@ pub(crate) async fn wasm_create_pipe(
pipe_id,
PendingPipeCreation {
generation,
token: token.clone(),
sender: tx,
},
);
let mut creation_guard = PendingPipeCreationGuard::new(
pending_pipe_creations.clone(),
expired_pipe_creations.clone(),
pipe_id,
generation,
token.clone(),
);
debug!(
target = "mtp.wasm",
pipe_id,
@ -218,31 +424,22 @@ pub(crate) async fn wasm_create_pipe(
"sending pipe request"
);
if let Err(error) = transport.send_frame(&request_bytes).await {
let mut pending = pending_pipe_creations.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == generation)
{
pending.remove(&pipe_id);
}
return Err(error);
}
if current_generation.get() != generation {
let mut pending = pending_pipe_creations.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == generation)
{
pending.remove(&pipe_id);
}
return Err(js_error("connection attempt superseded"));
}
creation_guard.disarm();
Ok(WasmPipeHandle {
pipe_id,
description: description.to_string(),
transport: transport.clone(),
response_rx: Rc::new(RefCell::new(Some(rx))),
pending: pending_pipe_creations.clone(),
expired: expired_pipe_creations.clone(),
generation,
token,
})
}
@ -282,6 +479,7 @@ pub(crate) async fn wasm_accept_pipe(
},
);
}
let _acceptance_guard = PendingPipeGuard::new(pending_pipes.clone(), pipe_id, generation);
debug!(
target = "mtp.wasm",
@ -291,28 +489,42 @@ pub(crate) async fn wasm_accept_pipe(
"sending pipe response"
);
if let Err(error) = transport.send_frame(&resp_bytes).await {
let mut pending = pending_pipes.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == generation)
{
pending.remove(&pipe_id);
}
return Err(error);
}
if current_generation.get() != generation {
let mut pending = pending_pipes.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == generation)
{
pending.remove(&pipe_id);
}
return Err(js_error("connection attempt superseded"));
}
rx.await
.map_err(|_| js_error("pipe closed before stream arrived"))?
let response = rx.fuse();
let timeout = wait_for_timeout(DEFAULT_PIPE_CREATION_TIMEOUT_MS).fuse();
pin_mut!(response, timeout);
let result = select! {
result = response => match result {
Ok(result) => result,
Err(_) => {
remove_pending_pipe(pending_pipes, pipe_id, generation);
return Err(js_error("pipe closed before stream arrived"));
}
},
result = timeout => {
result?;
remove_pending_pipe(pending_pipes, pipe_id, generation);
return Err(js_error(format!(
"pipe acceptance timed out after {DEFAULT_PIPE_CREATION_TIMEOUT_MS}ms"
)));
},
};
result
}
fn remove_pending_pipe(pending_pipes: &PendingPipes, pipe_id: u32, generation: u32) {
let mut pending = pending_pipes.borrow_mut();
if pending
.get(&pipe_id)
.is_some_and(|entry| entry.generation == generation)
{
pending.remove(&pipe_id);
}
}
pub(crate) async fn wasm_deny_pipe(transport: &WasmTransport, pipe_id: u32) -> Result<(), JsValue> {

View file

@ -1,5 +1,6 @@
use wasm_bindgen::prelude::*;
#[derive(Clone)]
#[wasm_bindgen]
pub struct ConnectionConfig {
pub(crate) url: String,

View file

@ -2,8 +2,8 @@ use wasm_bindgen::prelude::*;
use zeroize::Zeroizing;
use mtp_codec::{
DataValue, MtpProtectionPurpose, PROTOCOL_VERSION, ProtectionPolicy, ProtectionPurpose,
SealedRelayBuilder, SignaturePolicy, TypeMap,
DataValue, DecodeLimits, EncodeLimits, MtpProtectionPurpose, PROTOCOL_VERSION,
ProtectionPolicy, ProtectionPurpose, SealedRelayBuilder, SignaturePolicy, TypeMap,
};
use mtp_crypto::{
AeadDecrypt, AeadEncrypt, DualSigner, Ed25519Signer, HybridKem, KemPrivateKey, KemPublicKey,
@ -12,10 +12,18 @@ use mtp_crypto::{
};
use crate::error::{from_protection_error, js_error};
use crate::relay::{decode_frame, relay_error, structured_error};
use crate::relay::{decode_error, decode_frame, relay_error, structured_error};
fn decode_data_value(value: &[u8]) -> Result<DataValue, JsValue> {
DataValue::from_bytes(value).ok_or_else(|| js_error("invalid DataValue"))
DataValue::try_from_bytes_with_limits(value, DecodeLimits::default()).map_err(|error| {
let value = decode_error(error, "DataValue decoding failed");
let _ = js_sys::Reflect::set(
&value,
&JsValue::from_str("code"),
&JsValue::from_str("invalid-data-value"),
);
value
})
}
fn decode_public_key_bundle(
@ -74,10 +82,19 @@ pub struct WasmKeyring {
#[wasm_bindgen]
impl WasmKeyring {
/// Serialise the keyring to bytes.
/// Serialise the keyring to bytes and report malformed caller-owned
/// material as a JavaScript exception.
#[wasm_bindgen]
pub fn to_bytes(&self) -> Vec<u8> {
self.inner.to_bytes().to_vec()
pub fn to_bytes(&self) -> Result<Vec<u8>, JsValue> {
self.try_to_bytes()
}
#[wasm_bindgen]
pub fn try_to_bytes(&self) -> Result<Vec<u8>, JsValue> {
self.inner
.try_to_bytes()
.map(|bytes| bytes.to_vec())
.map_err(|error| js_error(format!("Keyring serialization failed: {error}")))
}
/// Deserialise a keyring from bytes.
@ -118,8 +135,17 @@ impl WasmKeyring {
/// Generate a full keyring with KEM, ML-DSA, and Ed25519 keys.
#[wasm_bindgen]
pub fn keyring_generate() -> Vec<u8> {
Keyring::generate().to_bytes().to_vec()
pub fn keyring_generate() -> Result<Vec<u8>, JsValue> {
keyring_generate_checked()
}
/// Generate a full keyring and report serialization failures to JavaScript.
#[wasm_bindgen]
pub fn keyring_generate_checked() -> Result<Vec<u8>, JsValue> {
Keyring::generate()
.try_to_bytes()
.map(|bytes| bytes.to_vec())
.map_err(|error| js_error(format!("generated keyring serialization failed: {error}")))
}
/// Build a [`Keyring`] containing only an Ed25519 keypair (no KEM, no ML-DSA).
@ -142,7 +168,10 @@ pub fn keyring_from_ed25519(secret_key: &[u8], public_key: &[u8]) -> Result<Vec<
SignaturePublicKey::new(public_key.to_vec()),
SignaturePrivateKey::new(secret_key.to_vec()),
);
Ok(keyring.to_bytes().to_vec())
keyring
.try_to_bytes()
.map(|bytes| bytes.to_vec())
.map_err(|error| js_error(format!("Keyring serialization failed: {error}")))
}
// ===========================================================================
@ -172,8 +201,15 @@ impl WasmPublicKeyBundle {
}
#[wasm_bindgen]
pub fn to_bytes(&self) -> Vec<u8> {
self.inner.as_bytes()
pub fn to_bytes(&self) -> Result<Vec<u8>, JsValue> {
self.try_to_bytes()
}
#[wasm_bindgen]
pub fn try_to_bytes(&self) -> Result<Vec<u8>, JsValue> {
self.inner
.try_as_bytes()
.map_err(|error| js_error(format!("public key bundle serialization failed: {error}")))
}
#[wasm_bindgen]
@ -428,6 +464,14 @@ pub fn wasm_sha256_double(data: &[u8]) -> Vec<u8> {
// KDF
// ===========================================================================
/// Length, in bytes, of symmetric keys produced by the MTP key-derivation
/// bindings. SDKs should query this instead of duplicating the crypto
/// primitive's output size.
#[wasm_bindgen]
pub fn mtp_symmetric_key_length() -> u32 {
32
}
/// HKDF-expand: derive `len` bytes from `ikm` with `salt` and `info`.
#[wasm_bindgen]
pub fn wasm_hkdf_expand(
@ -452,6 +496,21 @@ pub fn wasm_derive_encryption_key(
.map_err(|e| js_error(format!("derive_encryption_key failed: {}", e)))
}
/// Derive a 32-byte key from a passphrase using explicit Argon2id parameters.
/// The salt and parameters are part of the caller's protected-data format.
#[wasm_bindgen]
pub fn wasm_argon2id(
passphrase: &[u8],
salt: &[u8],
memory_kib: u32,
iterations: u32,
lanes: u32,
) -> Result<Vec<u8>, JsValue> {
mtp_crypto::derive_password_key(passphrase, salt, memory_kib, iterations, lanes)
.map(|key| key.to_vec())
.map_err(|e| js_error(format!("argon2id password derivation failed: {e}")))
}
/// Signature suites accepted by high-level protected-value APIs.
pub const PROTECTION_SIGNATURE_SUITE_ED25519: u8 = 0x01;
pub const PROTECTION_SIGNATURE_SUITE_DUAL: u8 = 0x03;
@ -564,10 +623,11 @@ pub fn verify_data_value_with_policy(
let value = decode_data_value(value)?;
let bundle = decode_public_key_bundle(public_key_bundle, None)?;
let result = if signature_suite == 0 {
value.verify(
value.verify_with_policy(
expected_signer_id,
&bundle,
ProtectionPurpose::from(expected_purpose),
ProtectionPolicy::any_supported(),
)
} else {
value.verify_with_policy(
@ -650,7 +710,11 @@ pub fn decrypt_data_value_with_keyrings(
let keyrings = keyrings_from_js(&keyrings)?;
let references: Vec<&Keyring> = keyrings.iter().collect();
value
.decrypt_with_keyrings(&references, ProtectionPurpose::from(expected_purpose))
.decrypt_with_keyrings_and_limits(
&references,
ProtectionPurpose::from(expected_purpose),
DecodeLimits::default(),
)
.map_err(from_protection_error)?
.to_bytes()
.map_err(|e| js_error(format!("decryption failed: {e}")))
@ -746,6 +810,13 @@ pub fn mtp_protection_signature_suite_dual() -> u8 {
PROTECTION_SIGNATURE_SUITE_DUAL
}
/// Explicit compatibility policy value accepting any signature suite
/// supported by this WASM build. New callers should prefer a fixed suite.
#[wasm_bindgen]
pub fn mtp_protection_signature_suite_any_supported() -> u8 {
0
}
/// Forward a sealed relay frame to another clear next hop without opening or
/// re-encoding its authenticated encrypted payload.
#[wasm_bindgen]
@ -776,12 +847,27 @@ fn build_encrypted_relay_frame_impl(
signer: &dyn SignatureScheme,
metadata_recipient_public_key_bundles: JsValue,
content_recipient_public_key_bundles: JsValue,
limits: JsValue,
) -> Result<Vec<u8>, JsValue> {
let tm = TypeMap::new(PROTOCOL_VERSION);
let application_content = crate::frame::js_to_data_value(&data, &tm)?;
let encode_limits = if limits.is_null() || limits.is_undefined() {
EncodeLimits::default()
} else {
crate::client::encode_limits_from_js(&limits)?
};
let relay_options =
crate::relay::relay_open_options(ProtectionPolicy::any_supported(), &limits)?;
let application_content =
crate::frame::js_to_data_value_with_limits(&data, &tm, encode_limits)?;
let application_metadata = encoded_metadata
.as_deref()
.map(decode_data_value)
.map(|bytes| {
DataValue::try_from_bytes_with_limits(
bytes,
DecodeLimits::for_transport_message_size(encode_limits.max_output_size as u64),
)
.map_err(|error| crate::relay::decode_error(error, "metadata decoding failed"))
})
.transpose()?;
let content_recipients = public_key_bundles_from_js(&content_recipient_public_key_bundles)?;
let metadata_recipients = public_key_bundles_from_js(&metadata_recipient_public_key_bundles)?;
@ -798,6 +884,8 @@ fn build_encrypted_relay_frame_impl(
.created_at(created_at)
.metadata_recipients(metadata_recipients)
.content_recipients(content_recipients)
.encode_limits(encode_limits)
.protected_limits(relay_options.protected_limits)
.type_map(&tm);
let builder = match application_metadata {
Some(metadata) => builder.metadata(metadata),
@ -807,7 +895,7 @@ fn build_encrypted_relay_frame_impl(
builder
.build()
.map_err(relay_error)?
.to_bytes()
.to_bytes_with_limits(encode_limits)
.map_err(|e| js_error(format!("relay frame encoding failed: {e}")))
}
@ -844,6 +932,45 @@ pub fn build_encrypted_relay_frame_with_keyring(
&signer,
metadata_recipient_public_key_bundles,
content_recipient_public_key_bundles,
JsValue::UNDEFINED,
)
}
/// Build a sealed relay frame with explicit encoder and semantic field
/// limits. The same limits are applied by the native relay builder.
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn build_encrypted_relay_frame_with_keyring_with_limits(
message_type: &str,
data: JsValue,
signer_id: u64,
final_recipient_id: u64,
next_hop_id: u64,
message_id: &str,
created_at: u64,
encoded_metadata: Option<Vec<u8>>,
keyring_bytes: &[u8],
signature_suite: u8,
metadata_recipient_public_key_bundles: JsValue,
content_recipient_public_key_bundles: JsValue,
limits: JsValue,
) -> Result<Vec<u8>, JsValue> {
let keyring = Keyring::from_bytes(keyring_bytes)
.map_err(|e| js_error(format!("keyring initialization failed: {e}")))?;
let signer = relay_signer_from_keyring(&keyring, signature_suite)?;
build_encrypted_relay_frame_impl(
message_type,
data,
signer_id,
final_recipient_id,
next_hop_id,
message_id,
created_at,
encoded_metadata,
&signer,
metadata_recipient_public_key_bundles,
content_recipient_public_key_bundles,
limits,
)
}
@ -891,7 +1018,7 @@ mod tests {
},
};
let bytes = bundle.to_bytes();
let bytes = bundle.try_to_bytes().expect("bundle serialization");
let restored = WasmPublicKeyBundle::from_bytes_unvalidated(&bytes)
.expect("from_bytes_unvalidated failed");
assert_eq!(restored.sig_cl_public_key(), pk);
@ -1098,7 +1225,7 @@ mod tests {
let value = DataValue::Str("signed through wasm".into())
.to_bytes()
.expect("value encoding failed");
let keyring_bytes = keyring.to_bytes();
let keyring_bytes = keyring.try_to_bytes().expect("keyring serialization");
let signed = sign_data_value_with_keyring(
&value,
0xfeed_beef,
@ -1111,7 +1238,7 @@ mod tests {
verify_data_value_with_policy(
&signed,
&bundle.as_bytes(),
&bundle.try_as_bytes().expect("bundle serialization"),
0xfeed_beef,
7,
PROTECTION_SIGNATURE_SUITE_ED25519,
@ -1121,7 +1248,7 @@ mod tests {
assert!(
verify_data_value_with_policy(
&signed,
&wrong_bundle.as_bytes(),
&wrong_bundle.try_as_bytes().expect("bundle serialization"),
0xfeed_beef,
7,
PROTECTION_SIGNATURE_SUITE_ED25519,
@ -1137,21 +1264,29 @@ mod tests {
let value = DataValue::Array(vec![DataValue::BoolTrue, DataValue::UnsignedNumber(42)])
.to_bytes()
.expect("value encoding failed");
let encrypted = encrypt_data_value(&value, &recipient.as_bytes(), 9)
.expect("encrypt_data_value failed");
let decrypted = decrypt_data_value(&encrypted, &keyring.to_bytes(), 9)
.expect("decrypt_data_value failed");
let recipient_bytes = recipient.try_as_bytes().expect("recipient serialization");
let encrypted =
encrypt_data_value(&value, &recipient_bytes, 9).expect("encrypt_data_value failed");
let keyring_bytes = keyring.try_to_bytes().expect("keyring serialization");
let decrypted =
decrypt_data_value(&encrypted, &keyring_bytes, 9).expect("decrypt_data_value failed");
assert_eq!(decrypted, value);
let second_keyring = Keyring::generate();
let second_recipient = second_keyring.public_key_bundle();
let recipients = js_sys::Array::new();
recipients.push(&js_sys::Uint8Array::from(&recipient.as_bytes()[..]));
recipients.push(&js_sys::Uint8Array::from(&second_recipient.as_bytes()[..]));
let second_recipient_bytes = second_recipient
.try_as_bytes()
.expect("second recipient serialization");
recipients.push(&js_sys::Uint8Array::from(&recipient_bytes[..]));
recipients.push(&js_sys::Uint8Array::from(&second_recipient_bytes[..]));
let multi = encrypt_data_value_for_recipients(&value, recipients.into(), 9)
.expect("multi-recipient encryption failed");
let opened_by_second = decrypt_data_value(&multi, &second_keyring.to_bytes(), 9)
let second_keyring_bytes = second_keyring
.try_to_bytes()
.expect("second keyring serialization");
let opened_by_second = decrypt_data_value(&multi, &second_keyring_bytes, 9)
.expect("second recipient could not decrypt");
assert_eq!(opened_by_second, value);
}

View file

@ -1,9 +1,13 @@
use wasm_bindgen::{JsCast, prelude::*};
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
use mtp_codec::{
CommunicationType, CommunicationValue, DataType, DataValue, DecodeLimits, EncodeLimits,
PROTOCOL_VERSION,
};
use mtp_type_map::TypeMap;
use crate::error::js_error;
use crate::relay::decode_error;
#[wasm_bindgen(typescript_custom_section)]
const PARSED_FRAME_TS: &'static str = r#"
@ -137,7 +141,48 @@ pub(crate) fn data_value_to_js(value: &DataValue, tm: &TypeMap) -> Result<JsValu
}
const MAX_SAFE_INT: f64 = 9007199254740991.0; // 2^53 - 1
struct JsDataValueEncodeContext {
limits: EncodeLimits,
values: usize,
}
impl JsDataValueEncodeContext {
fn visit(&mut self, depth: usize) -> Result<(), JsValue> {
if depth > self.limits.max_depth {
return Err(js_error("MTP DataValue nesting-depth limit exceeded"));
}
self.values = self
.values
.checked_add(1)
.ok_or_else(|| js_error("MTP DataValue value-count limit exceeded"))?;
if self.values > self.limits.max_values {
return Err(js_error("MTP DataValue value-count limit exceeded"));
}
Ok(())
}
}
pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
js_to_data_value_with_limits(value, tm, EncodeLimits::default())
}
pub(crate) fn js_to_data_value_with_limits(
value: &JsValue,
tm: &TypeMap,
limits: EncodeLimits,
) -> Result<DataValue, JsValue> {
let mut context = JsDataValueEncodeContext { limits, values: 0 };
js_to_data_value_with_context(value, tm, &mut context, 0)
}
fn js_to_data_value_with_context(
value: &JsValue,
tm: &TypeMap,
context: &mut JsDataValueEncodeContext,
depth: usize,
) -> Result<DataValue, JsValue> {
context.visit(depth)?;
if value.is_null() || value.is_undefined() {
return Ok(DataValue::Null);
}
@ -152,9 +197,17 @@ pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValu
}
if js_sys::Array::is_array(value) {
let array = js_sys::Array::from(value);
if array.length() as usize > context.limits.max_values {
return Err(js_error("MTP DataValue value-count limit exceeded"));
}
let mut values = Vec::with_capacity(array.length() as usize);
for item in array.iter() {
values.push(js_to_data_value(&item, tm)?);
values.push(js_to_data_value_with_context(
&item,
tm,
context,
depth + 1,
)?);
}
return Ok(DataValue::Array(values));
}
@ -198,6 +251,9 @@ pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValu
if value.is_object() {
let object = js_sys::Object::from(value.clone());
let keys = js_sys::Object::keys(&object);
if keys.length() as usize > context.limits.max_values {
return Err(js_error("MTP DataValue value-count limit exceeded"));
}
let mut entries = Vec::with_capacity(keys.length() as usize);
for key in keys.iter() {
let key = key
@ -212,7 +268,10 @@ pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValu
tm.version
))
})?;
entries.push((id, js_to_data_value(&value, tm)?));
entries.push((
id,
js_to_data_value_with_context(&value, tm, context, depth + 1)?,
));
}
return Ok(DataValue::Container(entries));
}
@ -282,15 +341,16 @@ fn apply_frame_options(
}
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
parse_frame_value_with_type_map(frame, &TypeMap::latest())
parse_frame_value_with_limits(frame, &TypeMap::latest(), DecodeLimits::default())
}
pub(crate) fn parse_frame_value_with_type_map(
pub(crate) fn parse_frame_value_with_limits(
frame: &[u8],
type_map: &TypeMap,
limits: DecodeLimits,
) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes_with(frame, type_map)
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let comm = CommunicationValue::try_from_bytes_with_type_map_and_limits(frame, type_map, limits)
.map_err(|error| decode_error(error, "parse failed"))?;
let tm = type_map;
let obj = js_sys::Object::new();
@ -355,8 +415,8 @@ pub fn build_ping_frame(
/// Parse an auth response frame into a JS object.
#[wasm_bindgen(unchecked_return_type = "AuthResponse")]
pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(response)
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let comm = CommunicationValue::try_from_bytes_with_limits(response, DecodeLimits::default())
.map_err(|error| decode_error(error, "parse failed"))?;
let connected = matches!(
comm.get_data(DataType::Connected),
@ -418,8 +478,8 @@ pub fn parse_auth_response(response: &[u8]) -> Result<JsValue, JsValue> {
/// Parse any MTP frame into the human-readable CommunicationValue display form.
#[wasm_bindgen]
pub fn format_frame(frame: &[u8]) -> Result<String, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let comm = CommunicationValue::try_from_bytes_with_limits(frame, DecodeLimits::default())
.map_err(|error| decode_error(error, "parse failed"))?;
Ok(comm.to_string())
}
@ -429,31 +489,80 @@ pub fn parse_frame(frame: &[u8]) -> Result<JsValue, JsValue> {
parse_frame_value(frame)
}
/// Parse a frame with the caller's bounded receive policy. The compatibility
/// `parse_frame` entry point retains the default policy for existing callers.
#[wasm_bindgen(unchecked_return_type = "ParsedFrame")]
pub fn parse_frame_with_limits(frame: &[u8], limits: JsValue) -> Result<JsValue, JsValue> {
let limits = crate::client::decode_limits_from_js(&limits)?;
parse_frame_value_with_limits(frame, &TypeMap::latest(), limits)
}
/// Parse a standalone serialized `DataValue` into the same structured form
/// used for frame payloads. Protected values remain opaque until the caller
/// explicitly opens and verifies them.
#[wasm_bindgen(unchecked_return_type = "ParsedDataValue")]
pub fn parse_data_value(value: &[u8]) -> Result<JsValue, JsValue> {
let value = DataValue::from_bytes(value).ok_or_else(|| js_error("invalid DataValue"))?;
parse_data_value_with_decode_limits(value, DecodeLimits::default())
}
fn parse_data_value_with_decode_limits(
value: &[u8],
limits: DecodeLimits,
) -> Result<JsValue, JsValue> {
let value = DataValue::try_from_bytes_with_limits(value, limits)
.map_err(|error| decode_error(error, "decode data value failed"))?;
let tm = TypeMap::new(PROTOCOL_VERSION);
data_value_to_js(&value, &tm)
}
/// Parse a standalone serialized `DataValue` with the caller's bounded
/// receive policy. The compatibility `parse_data_value` entry point retains
/// the default policy for existing callers.
#[wasm_bindgen(unchecked_return_type = "ParsedDataValue")]
pub fn parse_data_value_with_limits(value: &[u8], limits: JsValue) -> Result<JsValue, JsValue> {
let limits = crate::client::decode_limits_from_js(&limits)?;
parse_data_value_with_decode_limits(value, limits)
}
/// Encode one standalone `DataValue` using the negotiated/current type map.
#[wasm_bindgen]
pub fn encode_data_value(value: JsValue) -> Result<Vec<u8>, JsValue> {
encode_data_value_with_encode_limits(value, EncodeLimits::default())
}
fn encode_data_value_with_encode_limits(
value: JsValue,
limits: EncodeLimits,
) -> Result<Vec<u8>, JsValue> {
let tm = TypeMap::new(PROTOCOL_VERSION);
js_to_data_value(&value, &tm)?
.to_bytes()
js_to_data_value_with_limits(&value, &tm, limits)?
.to_bytes_with_limits(limits)
.map_err(|e| js_error(format!("encode data value failed: {e}")))
}
/// Encode one standalone `DataValue` using explicit recursion and output
/// limits. The compatibility entry point above keeps the historical default.
#[wasm_bindgen]
pub fn encode_data_value_with_limits(value: JsValue, limits: JsValue) -> Result<Vec<u8>, JsValue> {
let limits = crate::client::encode_limits_from_js(&limits)?;
encode_data_value_with_encode_limits(value, limits)
}
/// Build a typed MTP frame using generated communication/data type names.
#[wasm_bindgen]
pub fn build_frame(
message_type: &str,
data: JsValue,
options: JsValue,
) -> Result<Vec<u8>, JsValue> {
build_frame_with_encode_limits(message_type, data, options, EncodeLimits::default())
}
fn build_frame_with_encode_limits(
message_type: &str,
data: JsValue,
options: JsValue,
limits: EncodeLimits,
) -> Result<Vec<u8>, JsValue> {
let comm_type = CommunicationType::from_name(message_type)
.ok_or_else(|| js_error(format!("unknown communication type: {message_type}")))?;
@ -478,7 +587,7 @@ pub fn build_frame(
))
})?;
msg = msg
.add_data(id, js_to_data_value(&value, &tm)?)
.add_data(id, js_to_data_value_with_limits(&value, &tm, limits)?)
.map_err(|e| js_error(format!("add data failed: {e}")))?;
}
} else if !data.is_null() && !data.is_undefined() {
@ -487,10 +596,24 @@ pub fn build_frame(
));
}
msg.to_bytes()
msg.to_bytes_with_limits(limits)
.map_err(|e| js_error(format!("encode failed: {}", e)))
}
/// Build a typed frame with explicit recursion and complete-frame output
/// limits. High-level SDK sends use this entry point with the transport's
/// admitted message size.
#[wasm_bindgen]
pub fn build_frame_with_limits(
message_type: &str,
data: JsValue,
options: JsValue,
limits: JsValue,
) -> Result<Vec<u8>, JsValue> {
let limits = crate::client::encode_limits_from_js(&limits)?;
build_frame_with_encode_limits(message_type, data, options, limits)
}
/// Build a typed MTP frame around a complete serialized `DataValue` payload.
///
/// Unlike [`build_frame`], this does not interpret the payload as a clear data
@ -501,19 +624,50 @@ pub fn build_frame_with_payload(
message_type: &str,
serialized_payload: &[u8],
options: JsValue,
) -> Result<Vec<u8>, JsValue> {
build_frame_with_payload_with_encode_limits(
message_type,
serialized_payload,
options,
EncodeLimits::default(),
)
}
fn build_frame_with_payload_with_encode_limits(
message_type: &str,
serialized_payload: &[u8],
options: JsValue,
limits: EncodeLimits,
) -> Result<Vec<u8>, JsValue> {
let comm_type = CommunicationType::from_name(message_type)
.ok_or_else(|| js_error(format!("unknown communication type: {message_type}")))?;
let payload = DataValue::from_bytes(serialized_payload)
.ok_or_else(|| js_error("invalid serialized DataValue payload"))?;
let payload = DataValue::try_from_bytes_with_limits(
serialized_payload,
DecodeLimits::for_transport_message_size(limits.max_output_size as u64),
)
.map_err(|error| decode_error(error, "invalid serialized DataValue payload"))?;
let message =
apply_frame_options(CommunicationValue::new(comm_type), &options)?.with_payload(payload);
message
.to_bytes()
.to_bytes_with_limits(limits)
.map_err(|e| js_error(format!("encode failed: {e}")))
}
/// Build a typed frame around a serialized payload with explicit output
/// limits. The payload is also parsed with a policy derived from that limit so
/// an oversized/deep input cannot bypass the bounded builder.
#[wasm_bindgen]
pub fn build_frame_with_payload_with_limits(
message_type: &str,
serialized_payload: &[u8],
options: JsValue,
limits: JsValue,
) -> Result<Vec<u8>, JsValue> {
let limits = crate::client::encode_limits_from_js(&limits)?;
build_frame_with_payload_with_encode_limits(message_type, serialized_payload, options, limits)
}
#[cfg(test)]
#[cfg(target_arch = "wasm32")]
mod tests {

View file

@ -1,7 +1,8 @@
use wasm_bindgen::prelude::*;
use mtp_codec::{
DataValue, ProtectedError, ProtectedMessageBuilder, ProtectedOpenOptions, ProtectionError,
DataValue, DecodeLimits, EncodeLimits, ProtectedError, ProtectedLimits,
ProtectedMessageBuilder, ProtectedOpenOptions, ProtectionError, ProtectionPolicy,
ProtectionPurpose, VerifiedProtectedMessage,
};
@ -9,7 +10,7 @@ use crate::crypto::{
keyrings_from_js, protection_policy_from_suite, public_key_bundles_from_js,
relay_signer_from_keyring,
};
use crate::relay::{decode_frame, structured_error};
use crate::relay::{decode_error, decode_frame_with_limits, structured_error};
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
@ -45,9 +46,86 @@ fn optional_u64(value: &JsValue, name: &str) -> Result<Option<u64>, JsValue> {
))
}
fn decode_data_value(value: &[u8]) -> Result<DataValue, JsValue> {
DataValue::from_bytes(value)
.ok_or_else(|| structured_error("invalid-data-value", "invalid DataValue"))
fn limit_usize(options: &JsValue, key: &str, default: usize) -> Result<usize, JsValue> {
if options.is_null() || options.is_undefined() {
return Ok(default);
}
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
if value.is_null() || value.is_undefined() {
return Ok(default);
}
let Some(number) = value.as_f64() else {
return Err(structured_error(
"invalid-limit",
format!("{key} must be a number"),
));
};
if !number.is_finite() || number.fract() != 0.0 || number < 0.0 {
return Err(structured_error(
"invalid-limit",
format!("{key} must be a non-negative integer"),
));
}
usize::try_from(number as u64)
.map_err(|_| structured_error("invalid-limit", format!("{key} is out of range")))
}
fn protected_open_options(
expected_receiver_id: Option<u64>,
signature_purpose: u8,
encryption_purpose: u8,
policy: mtp_codec::ProtectionPolicy,
limits: &JsValue,
) -> Result<ProtectedOpenOptions, JsValue> {
let defaults = DecodeLimits::default();
let encode_defaults = EncodeLimits::default();
let protected_defaults = ProtectedLimits::default();
let decode_limits = DecodeLimits {
max_depth: limit_usize(limits, "maxDepth", defaults.max_depth)?,
max_values: limit_usize(limits, "maxValues", defaults.max_values)?,
max_blob_size: limit_usize(limits, "maxBlobSize", defaults.max_blob_size)?,
max_recipients: limit_usize(limits, "maxRecipients", defaults.max_recipients)?,
max_allocated_bytes: limit_usize(
limits,
"maxAllocatedBytes",
defaults.max_allocated_bytes,
)?,
};
let encode_limits = EncodeLimits {
max_depth: limit_usize(limits, "maxDepth", encode_defaults.max_depth)?,
max_values: limit_usize(limits, "maxValues", encode_defaults.max_values)?,
max_output_size: limit_usize(limits, "maxOutputSize", encode_defaults.max_output_size)?,
};
let protected_limits = ProtectedLimits {
max_message_id_bytes: limit_usize(
limits,
"maxMessageIdBytes",
protected_defaults.max_message_id_bytes,
)?,
max_metadata_encoded_bytes: limit_usize(
limits,
"maxMetadataEncodedBytes",
protected_defaults.max_metadata_encoded_bytes,
)?,
max_signer_key_history: limit_usize(
limits,
"maxSignerKeyHistory",
protected_defaults.max_signer_key_history,
)?,
max_decryption_key_history: limit_usize(
limits,
"maxDecryptionKeyHistory",
protected_defaults.max_decryption_key_history,
)?,
};
Ok(ProtectedOpenOptions::new(
expected_receiver_id,
ProtectionPurpose::from(signature_purpose),
ProtectionPurpose::from(encryption_purpose),
policy,
)
.with_limits(decode_limits, protected_limits)
.with_encode_limits(encode_limits))
}
pub(crate) fn protected_error(error: ProtectedError) -> JsValue {
@ -86,6 +164,7 @@ fn protected_error_code(error: &ProtectedError) -> &'static str {
ProtectedError::ExpectedReceiverMismatch => "receiver-id-mismatch",
ProtectedError::ReservedApplicationType(_) => "reserved-application-type",
ProtectedError::Replay => "replay",
ProtectedError::ResourceLimit(_) => "resource-limit",
ProtectedError::ReplayGuard(_) => "replay-guard-error",
ProtectedError::Protection(error) => match error {
ProtectionError::NoMatchingRecipient => "no-matching-recipient",
@ -94,6 +173,7 @@ fn protected_error_code(error: &ProtectedError) -> &'static str {
ProtectionError::PurposeMismatch { .. } => "purpose-mismatch",
ProtectionError::SignerIdMismatch { .. } => "signer-id-mismatch",
ProtectionError::SignerKeyNotFound(_) => "signer-key-not-found",
ProtectionError::ResourceLimit(_) => "resource-limit",
ProtectionError::Crypto(mtp_crypto::CryptoError::InvalidSignature)
| ProtectionError::Crypto(mtp_crypto::CryptoError::VerificationFailed) => {
"invalid-signature"
@ -112,15 +192,6 @@ fn serialize_data_value(value: &DataValue) -> Result<Vec<u8>, JsValue> {
})
}
fn serialize_frame(frame: &mtp_codec::CommunicationValue) -> Result<Vec<u8>, JsValue> {
frame.to_bytes().map_err(|error| {
structured_error(
"invalid-frame",
format!("protected frame encoding failed: {error}"),
)
})
}
#[wasm_bindgen]
pub struct WasmVerifiedProtectedMessage {
inner: VerifiedProtectedMessage,
@ -181,7 +252,58 @@ pub fn build_protected_frame_with_keyring(
expose_sender: bool,
recipient_public_key_bundles: JsValue,
) -> Result<Vec<u8>, JsValue> {
let content = decode_data_value(encoded_content)?;
build_protected_frame_with_keyring_impl(
message_type,
encoded_content,
signer_id,
final_recipient_id,
message_id,
created_at,
signature_purpose,
encryption_purpose,
keyring_bytes,
signature_suite,
frame_id,
expose_sender,
recipient_public_key_bundles,
JsValue::UNDEFINED,
)
}
#[allow(clippy::too_many_arguments)]
fn build_protected_frame_with_keyring_impl(
message_type: &str,
encoded_content: &[u8],
signer_id: u64,
final_recipient_id: u64,
message_id: &str,
created_at: u64,
signature_purpose: u8,
encryption_purpose: u8,
keyring_bytes: &[u8],
signature_suite: u8,
frame_id: Option<u32>,
expose_sender: bool,
recipient_public_key_bundles: JsValue,
limits: JsValue,
) -> Result<Vec<u8>, JsValue> {
let encode_limits = if limits.is_null() || limits.is_undefined() {
EncodeLimits::default()
} else {
crate::client::encode_limits_from_js(&limits)?
};
let open_options = protected_open_options(
None,
signature_purpose,
encryption_purpose,
ProtectionPolicy::any_supported(),
&limits,
)?;
let content = DataValue::try_from_bytes_with_limits(
encoded_content,
DecodeLimits::for_transport_message_size(encode_limits.max_output_size as u64),
)
.map_err(|error| decode_error(error, "DataValue decoding failed"))?;
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes).map_err(|error| {
structured_error(
"invalid-keyring",
@ -202,42 +324,149 @@ pub fn build_protected_frame_with_keyring(
.message_id(message_id)
.created_at(created_at)
.recipients(recipients)
.encode_limits(encode_limits)
.protected_limits(open_options.protected_limits)
.expose_sender(expose_sender);
if let Some(frame_id) = frame_id {
builder = builder.frame_id(frame_id);
}
let frame = builder.build().map_err(protected_error)?;
serialize_frame(&frame)
frame.to_bytes_with_limits(encode_limits).map_err(|error| {
structured_error(
"invalid-frame",
format!("protected frame encoding failed: {error}"),
)
})
}
/// Build a complete encrypted protected frame with explicit encoder and
/// semantic protected-field limits.
#[wasm_bindgen]
#[allow(clippy::too_many_arguments)]
pub fn build_protected_frame_with_keyring_with_limits(
message_type: &str,
encoded_content: &[u8],
signer_id: u64,
final_recipient_id: u64,
message_id: &str,
created_at: u64,
signature_purpose: u8,
encryption_purpose: u8,
keyring_bytes: &[u8],
signature_suite: u8,
frame_id: Option<u32>,
expose_sender: bool,
recipient_public_key_bundles: JsValue,
limits: JsValue,
) -> Result<Vec<u8>, JsValue> {
build_protected_frame_with_keyring_impl(
message_type,
encoded_content,
signer_id,
final_recipient_id,
message_id,
created_at,
signature_purpose,
encryption_purpose,
keyring_bytes,
signature_suite,
frame_id,
expose_sender,
recipient_public_key_bundles,
limits,
)
}
/// Read the claimed, unverified signer ID after decrypting the protected
/// payload. The result may only select trusted keys for the same signer ID.
#[wasm_bindgen]
#[deprecated(note = "use protected_claimed_signer_id_with_limits")]
pub fn protected_claimed_signer_id(
frame: &[u8],
keyrings: JsValue,
encryption_purpose: u8,
) -> Result<u64, JsValue> {
let frame = decode_frame(frame)?;
protected_claimed_signer_id_impl(frame, keyrings, encryption_purpose, JsValue::UNDEFINED)
}
fn protected_claimed_signer_id_impl(
frame: &[u8],
keyrings: JsValue,
encryption_purpose: u8,
limits: JsValue,
) -> Result<u64, JsValue> {
let options = protected_open_options(
None,
0,
encryption_purpose,
ProtectionPolicy::any_supported(),
&limits,
)?;
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
let keyrings = keyrings_from_js(&keyrings).map_err(|error| {
structured_error(
"invalid-recipient-keyrings",
error.as_string().unwrap_or_default(),
)
})?;
if keyrings.len() > options.protected_limits.max_decryption_key_history {
return Err(protected_error(ProtectedError::ResourceLimit(
"decryption key history",
)));
}
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
mtp_codec::protected_claimed_signer_id(
mtp_codec::protected_claimed_signer_id_with_options(
&frame,
&references,
ProtectionPurpose::from(encryption_purpose),
options.decode_limits,
options.protected_limits,
)
.map_err(protected_error)
}
/// Open and verify a direct protected message in the native codec using
/// trusted signer-key history supplied by the SDK.
#[wasm_bindgen]
pub fn open_protected_with_keyrings(
pub fn protected_claimed_signer_id_with_limits(
frame: &[u8],
keyrings: JsValue,
encryption_purpose: u8,
limits: JsValue,
) -> Result<u64, JsValue> {
let options = protected_open_options(
None,
0,
encryption_purpose,
ProtectionPolicy::any_supported(),
&limits,
)?;
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
let keyrings = keyrings_from_js(&keyrings).map_err(|error| {
structured_error(
"invalid-recipient-keyrings",
error.as_string().unwrap_or_default(),
)
})?;
if keyrings.len() > options.protected_limits.max_decryption_key_history {
return Err(protected_error(ProtectedError::ResourceLimit(
"decryption key history",
)));
}
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
mtp_codec::protected_claimed_signer_id_with_options(
&frame,
&references,
ProtectionPurpose::from(encryption_purpose),
options.decode_limits,
options.protected_limits,
)
.map_err(protected_error)
}
/// Open a protected value without replay protection. This raw entry point is
/// intended for stored/forensic messages; message-processing callers should
/// apply their replay guard in the SDK or use a checked native API.
#[wasm_bindgen]
pub fn open_protected_with_keyrings_without_replay(
frame: &[u8],
keyrings: JsValue,
expected_signer_id: JsValue,
@ -247,7 +476,30 @@ pub fn open_protected_with_keyrings(
encryption_purpose: u8,
signature_suite: u8,
) -> Result<WasmVerifiedProtectedMessage, JsValue> {
let frame = decode_frame(frame)?;
open_protected_with_keyrings_impl(
frame,
keyrings,
expected_signer_id,
signer_public_key_bundles,
expected_receiver_id,
signature_purpose,
encryption_purpose,
signature_suite,
JsValue::UNDEFINED,
)
}
fn open_protected_with_keyrings_impl(
frame: &[u8],
keyrings: JsValue,
expected_signer_id: JsValue,
signer_public_key_bundles: JsValue,
expected_receiver_id: JsValue,
signature_purpose: u8,
encryption_purpose: u8,
signature_suite: u8,
limits: JsValue,
) -> Result<WasmVerifiedProtectedMessage, JsValue> {
let keyrings = keyrings_from_js(&keyrings).map_err(|error| {
structured_error(
"invalid-recipient-keyrings",
@ -268,23 +520,53 @@ pub fn open_protected_with_keyrings(
error.as_string().unwrap_or_default(),
)
})?;
let message = mtp_codec::open_protected_with_keys(
let options = protected_open_options(
expected_receiver_id,
signature_purpose,
encryption_purpose,
policy,
&limits,
)?;
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
let message = mtp_codec::open_protected_with_keys_without_replay(
&frame,
&references,
expected_signer_id,
&signer_public_keys,
ProtectedOpenOptions::new(
expected_receiver_id,
ProtectionPurpose::from(signature_purpose),
ProtectionPurpose::from(encryption_purpose),
policy,
),
None,
options,
)
.map_err(protected_error)?;
Ok(WasmVerifiedProtectedMessage { inner: message })
}
/// Open a bounded protected value without replay protection. The raw WASM
/// boundary cannot accept a native replay-guard trait, so message-processing
/// callers must use the SDK guard or a native checked API.
#[wasm_bindgen]
pub fn open_protected_with_keyrings_with_limits_without_replay(
frame: &[u8],
keyrings: JsValue,
expected_signer_id: JsValue,
signer_public_key_bundles: JsValue,
expected_receiver_id: JsValue,
signature_purpose: u8,
encryption_purpose: u8,
signature_suite: u8,
limits: JsValue,
) -> Result<WasmVerifiedProtectedMessage, JsValue> {
open_protected_with_keyrings_impl(
frame,
keyrings,
expected_signer_id,
signer_public_key_bundles,
expected_receiver_id,
signature_purpose,
encryption_purpose,
signature_suite,
limits,
)
}
#[cfg(all(test, target_arch = "wasm32"))]
mod tests {
use super::*;
@ -358,9 +640,12 @@ mod tests {
sender: &mtp_crypto::Keyring,
recipient: &mtp_crypto::Keyring,
) -> JsValue {
let recipient_bytes = recipient.to_bytes();
let signer_bundle_bytes = sender.public_key_bundle().as_bytes();
match open_protected_with_keyrings(
let recipient_bytes = recipient.try_to_bytes().expect("recipient serialization");
let signer_bundle_bytes = sender
.public_key_bundle()
.try_as_bytes()
.expect("signer bundle serialization");
match open_protected_with_keyrings_without_replay(
frame,
js_sys::Uint8Array::from(&recipient_bytes[..]).into(),
JsValue::bigint_from_str("7"),
@ -379,8 +664,11 @@ mod tests {
fn protected_builder_returns_the_complete_frame() {
let sender = mtp_crypto::Keyring::generate();
let recipient = mtp_crypto::Keyring::generate();
let sender_bytes = sender.to_bytes();
let recipient_bundle_bytes = recipient.public_key_bundle().as_bytes();
let sender_bytes = sender.try_to_bytes().expect("sender serialization");
let recipient_bundle_bytes = recipient
.public_key_bundle()
.try_as_bytes()
.expect("recipient bundle serialization");
let content = DataValue::Str("complete-frame".into())
.to_bytes()
.expect("encode content");

View file

@ -1,8 +1,8 @@
use wasm_bindgen::prelude::*;
use mtp_codec::{
CommunicationValue, DataValue, ProtectionError, RelayError, VerifiedRelayContent,
VerifiedRelayMetadata,
CommunicationValue, DataValue, DecodeLimits, EncodeLimits, ProtectedLimits, ProtectionError,
ProtectionPolicy, RelayError, RelayOpenOptions, VerifiedRelayContent, VerifiedRelayMetadata,
};
use crate::crypto::{keyrings_from_js, protection_policy_from_suite, public_key_bundles_from_js};
@ -16,6 +16,28 @@ pub(crate) fn structured_error(code: &str, message: impl Into<String>) -> JsValu
value
}
pub(crate) fn decode_error(error: mtp_codec::DecodeError, context: &str) -> JsValue {
let value = structured_error("invalid-frame", format!("{context}: {error}"));
let _ = js_sys::Reflect::set(
&value,
&JsValue::from_str("decodeCode"),
&JsValue::from_str(decode_error_code(&error)),
);
value
}
pub(crate) fn decode_error_code(error: &mtp_codec::DecodeError) -> &'static str {
match error {
mtp_codec::DecodeError::MalformedEncoding => "malformed-encoding",
mtp_codec::DecodeError::DepthLimit => "depth-limit",
mtp_codec::DecodeError::ValueCountLimit => "value-count-limit",
mtp_codec::DecodeError::BlobLimit => "blob-limit",
mtp_codec::DecodeError::AllocationLimit => "allocation-limit",
mtp_codec::DecodeError::RecipientLimit => "recipient-limit",
mtp_codec::DecodeError::DuplicateField => "duplicate-field",
}
}
fn wrapped_input_error(code: &str, error: JsValue) -> JsValue {
let message = error
.as_string()
@ -54,6 +76,7 @@ fn relay_error_code(error: &RelayError) -> &'static str {
RelayError::UnsupportedRelayVersion(_) => "unsupported-relay-version",
RelayError::NotFinalRecipient => "not-final-recipient",
RelayError::Replay => "replay",
RelayError::ResourceLimit(_) => "resource-limit",
RelayError::ReservedApplicationType(_) => "reserved-application-type",
RelayError::ReplayGuard(_) => "replay-guard-error",
RelayError::Protection(error) => match error {
@ -63,6 +86,7 @@ fn relay_error_code(error: &RelayError) -> &'static str {
ProtectionError::PurposeMismatch { .. } => "purpose-mismatch",
ProtectionError::SignerIdMismatch { .. } => "signer-id-mismatch",
ProtectionError::SignerKeyNotFound(_) => "signer-key-not-found",
ProtectionError::ResourceLimit(_) => "resource-limit",
ProtectionError::Crypto(mtp_crypto::CryptoError::InvalidSignature)
| ProtectionError::Crypto(mtp_crypto::CryptoError::VerificationFailed) => {
"invalid-signature"
@ -73,12 +97,15 @@ fn relay_error_code(error: &RelayError) -> &'static str {
}
pub(crate) fn decode_frame(frame: &[u8]) -> Result<CommunicationValue, JsValue> {
CommunicationValue::from_bytes(frame).map_err(|error| {
structured_error(
"invalid-frame",
format!("relay frame decoding failed: {error}"),
)
})
decode_frame_with_limits(frame, DecodeLimits::default())
}
pub(crate) fn decode_frame_with_limits(
frame: &[u8],
limits: DecodeLimits,
) -> Result<CommunicationValue, JsValue> {
CommunicationValue::try_from_bytes_with_limits(frame, limits)
.map_err(|error| decode_error(error, "relay frame decoding failed"))
}
fn optional_u64(value: &JsValue, name: &str) -> Result<Option<u64>, JsValue> {
@ -113,6 +140,79 @@ fn optional_u64(value: &JsValue, name: &str) -> Result<Option<u64>, JsValue> {
))
}
fn limit_usize(options: &JsValue, key: &str, default: usize) -> Result<usize, JsValue> {
if options.is_null() || options.is_undefined() {
return Ok(default);
}
let value = js_sys::Reflect::get(options, &JsValue::from_str(key))?;
if value.is_null() || value.is_undefined() {
return Ok(default);
}
let Some(number) = value.as_f64() else {
return Err(structured_error(
"invalid-limit",
format!("{key} must be a number"),
));
};
if !number.is_finite() || number.fract() != 0.0 || number < 0.0 {
return Err(structured_error(
"invalid-limit",
format!("{key} must be a non-negative integer"),
));
}
usize::try_from(number as u64)
.map_err(|_| structured_error("invalid-limit", format!("{key} is out of range")))
}
pub(crate) fn relay_open_options(
policy: mtp_codec::ProtectionPolicy,
limits: &JsValue,
) -> Result<RelayOpenOptions, JsValue> {
let decode_defaults = DecodeLimits::default();
let encode_defaults = EncodeLimits::default();
let protected_defaults = ProtectedLimits::default();
let options = RelayOpenOptions::new(policy).with_limits(
DecodeLimits {
max_depth: limit_usize(limits, "maxDepth", decode_defaults.max_depth)?,
max_values: limit_usize(limits, "maxValues", decode_defaults.max_values)?,
max_blob_size: limit_usize(limits, "maxBlobSize", decode_defaults.max_blob_size)?,
max_recipients: limit_usize(limits, "maxRecipients", decode_defaults.max_recipients)?,
max_allocated_bytes: limit_usize(
limits,
"maxAllocatedBytes",
decode_defaults.max_allocated_bytes,
)?,
},
ProtectedLimits {
max_message_id_bytes: limit_usize(
limits,
"maxMessageIdBytes",
protected_defaults.max_message_id_bytes,
)?,
max_metadata_encoded_bytes: limit_usize(
limits,
"maxMetadataEncodedBytes",
protected_defaults.max_metadata_encoded_bytes,
)?,
max_signer_key_history: limit_usize(
limits,
"maxSignerKeyHistory",
protected_defaults.max_signer_key_history,
)?,
max_decryption_key_history: limit_usize(
limits,
"maxDecryptionKeyHistory",
protected_defaults.max_decryption_key_history,
)?,
},
);
Ok(options.with_encode_limits(EncodeLimits {
max_depth: limit_usize(limits, "maxDepth", encode_defaults.max_depth)?,
max_values: limit_usize(limits, "maxValues", encode_defaults.max_values)?,
max_output_size: limit_usize(limits, "maxOutputSize", encode_defaults.max_output_size)?,
}))
}
fn serialize_data_value(value: &DataValue) -> Result<Vec<u8>, JsValue> {
value.to_bytes().map_err(|error| {
structured_error(
@ -196,19 +296,49 @@ impl WasmVerifiedRelayContent {
/// versioned relay metadata parser in the JavaScript SDK. The caller must bind
/// this value as the expected signer during the subsequent verification call.
#[wasm_bindgen]
#[deprecated(note = "use relay_metadata_claimed_signer_id_with_limits")]
pub fn relay_metadata_claimed_signer_id(frame: &[u8], keyrings: JsValue) -> Result<u64, JsValue> {
let frame = decode_frame(frame)?;
relay_metadata_claimed_signer_id_impl(frame, keyrings, JsValue::UNDEFINED)
}
fn relay_metadata_claimed_signer_id_impl(
frame: &[u8],
keyrings: JsValue,
limits: JsValue,
) -> Result<u64, JsValue> {
let options = relay_open_options(ProtectionPolicy::any_supported(), &limits)?;
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
let keyrings = keyrings_from_js(&keyrings)
.map_err(|error| wrapped_input_error("invalid-recipient-keyrings", error))?;
if keyrings.len() > options.protected_limits.max_decryption_key_history {
return Err(relay_error(RelayError::ResourceLimit(
"decryption key history",
)));
}
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
mtp_codec::relay_metadata_claimed_signer_id(&frame, &references).map_err(relay_error)
mtp_codec::relay_metadata_claimed_signer_id_with_options(
&frame,
&references,
options.decode_limits,
options.protected_limits,
)
.map_err(relay_error)
}
/// Open and verify relay metadata in the native codec. JavaScript resolves
/// the trusted signing-key history before calling this function, while the
/// codec owns all relay layout and version interpretation.
#[wasm_bindgen]
pub fn open_relay_metadata_with_keyrings(
pub fn relay_metadata_claimed_signer_id_with_limits(
frame: &[u8],
keyrings: JsValue,
limits: JsValue,
) -> Result<u64, JsValue> {
relay_metadata_claimed_signer_id_impl(frame, keyrings, limits)
}
/// Open relay metadata without replay protection. This raw entry point is for
/// stored/forwarded messages; message-processing paths should add a guard in
/// the SDK or use the checked native API.
#[wasm_bindgen]
pub fn open_relay_metadata_with_keyrings_without_replay(
frame: &[u8],
keyrings: JsValue,
expected_signer_id: JsValue,
@ -225,21 +355,54 @@ pub fn open_relay_metadata_with_keyrings(
.ok_or_else(|| structured_error("invalid-option", "expectedSignerId is required"))?;
let policy = protection_policy_from_suite(signature_suite)
.map_err(|error| wrapped_input_error("unsupported-signature-suite", error))?;
let metadata = mtp_codec::open_relay_metadata_with_keys(
let metadata = mtp_codec::open_relay_metadata_with_limits_without_replay(
&frame,
&references,
expected_signer_id,
&signer_public_keys,
policy,
Some(expected_signer_id),
move |_| Some(signer_public_keys),
RelayOpenOptions::new(policy),
)
.map_err(relay_error)?;
Ok(WasmVerifiedRelayMetadata { inner: metadata })
}
/// Open and verify relay content in the native codec using recipient and
/// signer key histories supplied by the SDK.
/// Open bounded relay metadata without replay protection. Use the SDK's
/// message-processing guard or a native checked API for live traffic.
#[wasm_bindgen]
pub fn open_relay_content_with_keyrings(
pub fn open_relay_metadata_with_keyrings_with_limits_without_replay(
frame: &[u8],
keyrings: JsValue,
expected_signer_id: JsValue,
signer_public_key_bundles: JsValue,
signature_suite: u8,
limits: JsValue,
) -> Result<WasmVerifiedRelayMetadata, JsValue> {
let keyrings = keyrings_from_js(&keyrings)
.map_err(|error| wrapped_input_error("invalid-recipient-keyrings", error))?;
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
let signer_public_keys = public_key_bundles_from_js(&signer_public_key_bundles)
.map_err(|error| wrapped_input_error("invalid-signer-keys", error))?;
let expected_signer_id = optional_u64(&expected_signer_id, "expectedSignerId")?
.ok_or_else(|| structured_error("invalid-option", "expectedSignerId is required"))?;
let policy = protection_policy_from_suite(signature_suite)
.map_err(|error| wrapped_input_error("unsupported-signature-suite", error))?;
let options = relay_open_options(policy, &limits)?;
let frame = decode_frame_with_limits(frame, options.decode_limits)?;
let metadata = mtp_codec::open_relay_metadata_with_limits_without_replay(
&frame,
&references,
Some(expected_signer_id),
move |_| Some(signer_public_keys),
options,
)
.map_err(relay_error)?;
Ok(WasmVerifiedRelayMetadata { inner: metadata })
}
/// Open relay content without making a second replay decision. Replay is
/// consumed when live message processing accepts the authenticated metadata.
#[wasm_bindgen]
pub fn open_relay_content_with_keyrings_without_replay(
metadata: &WasmVerifiedRelayMetadata,
keyrings: JsValue,
signer_public_key_bundles: JsValue,
@ -255,12 +418,49 @@ pub fn open_relay_content_with_keyrings(
optional_u64(&expected_final_recipient_id, "expectedFinalRecipientId")?;
let policy = protection_policy_from_suite(signature_suite)
.map_err(|error| wrapped_input_error("unsupported-signature-suite", error))?;
let content = mtp_codec::open_relay_content_with_keyrings(
let content = mtp_codec::open_relay_content_with_limits_without_replay(
&metadata.inner,
&references,
&signer_public_keys,
expected_final_recipient_id,
policy,
RelayOpenOptions {
policy,
decode_limits: metadata.inner.decode_limits(),
encode_limits: metadata.inner.encode_limits(),
protected_limits: metadata.inner.protected_limits(),
},
)
.map_err(relay_error)?;
Ok(WasmVerifiedRelayContent { inner: content })
}
/// Open bounded relay content without replay protection. Replay is consumed
/// when metadata is accepted by the live SDK/native processing boundary.
#[wasm_bindgen]
pub fn open_relay_content_with_keyrings_with_limits_without_replay(
metadata: &WasmVerifiedRelayMetadata,
keyrings: JsValue,
signer_public_key_bundles: JsValue,
expected_final_recipient_id: JsValue,
signature_suite: u8,
limits: JsValue,
) -> Result<WasmVerifiedRelayContent, JsValue> {
let keyrings = keyrings_from_js(&keyrings)
.map_err(|error| wrapped_input_error("invalid-recipient-keyrings", error))?;
let references: Vec<&mtp_crypto::Keyring> = keyrings.iter().collect();
let signer_public_keys = public_key_bundles_from_js(&signer_public_key_bundles)
.map_err(|error| wrapped_input_error("invalid-signer-keys", error))?;
let expected_final_recipient_id =
optional_u64(&expected_final_recipient_id, "expectedFinalRecipientId")?;
let policy = protection_policy_from_suite(signature_suite)
.map_err(|error| wrapped_input_error("unsupported-signature-suite", error))?;
let options = relay_open_options(policy, &limits)?;
let content = mtp_codec::open_relay_content_with_limits_without_replay(
&metadata.inner,
&references,
&signer_public_keys,
expected_final_recipient_id,
options,
)
.map_err(relay_error)?;
Ok(WasmVerifiedRelayContent { inner: content })

View file

@ -7,8 +7,8 @@ use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use crate::error::js_error;
use crate::frame::parse_frame_value_with_type_map;
use mtp_codec::TypeMap;
use crate::frame::parse_frame_value_with_limits;
use mtp_codec::{DecodeLimits, EncodeLimits, TypeMap};
const CLOSE_FRAME_LEN: u32 = u32::MAX;
@ -133,6 +133,7 @@ pub struct WasmTransport {
/// Serializes stream creation and writes across concurrent callers.
send_lock: Rc<AsyncMutex<()>>,
type_map: Rc<RefCell<TypeMap>>,
decode_limits: Rc<RefCell<DecodeLimits>>,
}
impl WasmTransport {
@ -140,6 +141,15 @@ impl WasmTransport {
url: &str,
cert_hashes: Option<Vec<String>>,
max_message_size: u32,
) -> Result<Self, JsValue> {
Self::connect_with_limits(url, cert_hashes, max_message_size, None).await
}
pub async fn connect_with_limits(
url: &str,
cert_hashes: Option<Vec<String>>,
max_message_size: u32,
configured_limits: Option<DecodeLimits>,
) -> Result<Self, JsValue> {
let ctor = js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("WebTransport"))?
.dyn_into::<js_sys::Function>()
@ -188,6 +198,10 @@ impl WasmTransport {
JsFuture::from(ready)
.await
.map_err(|e| js_error(format!("WebTransport ready failed: {:?}", e)))?;
let transport_limits = DecodeLimits::for_transport_message_size(max_message_size as u64);
let decode_limits = configured_limits
.map(|limits| restrict_decode_limits(limits, transport_limits))
.unwrap_or(transport_limits);
Ok(Self {
inner: transport,
max_message_size,
@ -198,6 +212,7 @@ impl WasmTransport {
outgoing_writer: Rc::new(RefCell::new(None)),
send_lock: Rc::new(AsyncMutex::new(())),
type_map: Rc::new(RefCell::new(TypeMap::latest())),
decode_limits: Rc::new(RefCell::new(decode_limits)),
})
}
@ -213,6 +228,17 @@ impl WasmTransport {
self.type_map.borrow().clone()
}
pub fn decode_limits(&self) -> DecodeLimits {
*self.decode_limits.borrow()
}
/// Encoder policy corresponding to the transport's admitted complete
/// frame size. SDK builders use this before constructing a frame so an
/// oversized value is rejected before its serialized buffer is created.
pub fn encode_limits(&self) -> EncodeLimits {
EncodeLimits::for_transport_message_size(self.max_message_size as u64)
}
pub async fn send_frame(&self, frame: &[u8]) -> Result<(), JsValue> {
let _send_guard = self.send_lock.lock().await;
if frame.len() as u64 > self.max_message_size as u64
@ -462,7 +488,7 @@ impl WasmTransport {
match self.next_frame(self.max_message_size).await {
Ok(FrameOutcome::Frame(frame)) => {
let type_map = self.type_map();
match parse_frame_value_with_type_map(&frame, &type_map) {
match parse_frame_value_with_limits(&frame, &type_map, self.decode_limits()) {
Ok(parsed) => {
on_message(parsed);
}
@ -498,15 +524,23 @@ impl WasmTransport {
match self.next_frame(self.max_message_size).await {
Ok(FrameOutcome::Frame(frame)) => {
let type_map = self.type_map();
let decode_limits = self.decode_limits();
let pipe_request_type =
mtp_codec::CommunicationType::PipeRequest.try_to_id(&type_map);
let pipe_response_type =
mtp_codec::CommunicationType::PipeResponse.try_to_id(&type_map);
let is_first = self.new_stream_frame.get();
let comm =
mtp_codec::CommunicationValue::try_from_bytes_with_type_map_and_limits(
&frame,
&type_map,
decode_limits,
)
.ok();
if is_first {
self.new_stream_frame.set(false);
if let Ok(comm) =
mtp_codec::CommunicationValue::from_bytes_with(&frame, &type_map)
if let Some(comm) = comm.as_ref()
&& Some(comm.get_type()) == pipe_request_type
{
let Some(pipe_id) = comm.id().filter(|id| *id != 0) else {
@ -539,8 +573,7 @@ impl WasmTransport {
}
}
if let Ok(comm) =
mtp_codec::CommunicationValue::from_bytes_with(&frame, &type_map)
if let Some(comm) = comm.as_ref()
&& Some(comm.get_type()) == pipe_response_type
&& !matches!(comm.id(), Some(id) if id != 0)
{
@ -551,8 +584,7 @@ impl WasmTransport {
break;
}
if let Ok(comm) =
mtp_codec::CommunicationValue::from_bytes_with(&frame, &type_map)
if let Some(comm) = comm.as_ref()
&& !matches!(comm.id(), Some(id) if id != 0)
&& comm
.get_type_name()
@ -565,7 +597,7 @@ impl WasmTransport {
break;
}
match parse_frame_value_with_type_map(&frame, &type_map) {
match parse_frame_value_with_limits(&frame, &type_map, decode_limits) {
Ok(parsed) => {
on_message(parsed);
}
@ -666,3 +698,13 @@ impl WasmTransport {
}
}
}
fn restrict_decode_limits(left: DecodeLimits, right: DecodeLimits) -> DecodeLimits {
DecodeLimits {
max_depth: left.max_depth.min(right.max_depth),
max_values: left.max_values.min(right.max_values),
max_blob_size: left.max_blob_size.min(right.max_blob_size),
max_recipients: left.max_recipients.min(right.max_recipients),
max_allocated_bytes: left.max_allocated_bytes.min(right.max_allocated_bytes),
}
}

1132
wasm/types/mtp_wasm.d.ts vendored

File diff suppressed because it is too large Load diff