[WIP] Security work While on holiday

This commit is contained in:
Alex 2026-08-12 22:45:28 +02:00
commit 7f0231e3f1
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
109 changed files with 19694 additions and 5210 deletions

View file

@ -7,12 +7,40 @@ use crate::error::js_error;
#[wasm_bindgen(typescript_custom_section)]
const PARSED_FRAME_TS: &'static str = r#"
export interface ParsedEncryptedValue {
kind: "encrypted";
encryptionType: number;
purpose: number;
recipientCount: number;
encoded: Uint8Array;
}
export interface ParsedSignedValue {
kind: "signed";
signatureType: number;
purpose: number;
signerId: bigint;
value: ParsedDataValue;
}
export type ParsedDataValue =
| boolean
| number
| bigint
| string
| Uint8Array
| ParsedDataValue[]
| { [key: string]: ParsedDataValue }
| ParsedEncryptedValue
| ParsedSignedValue
| null;
export interface ParsedFrame {
id?: number;
type: string;
sender?: bigint;
receiver?: bigint;
data: Record<string, unknown>;
data: ParsedDataValue;
raw: Uint8Array;
}
"#;
@ -28,10 +56,10 @@ fn integer_value(value: &str) -> JsValue {
{
return JsValue::from_f64(number);
}
JsValue::from_str(value)
JsValue::bigint_from_str(value)
}
fn data_value_to_js(value: &DataValue, tm: &TypeMap) -> Result<JsValue, JsValue> {
pub(crate) fn data_value_to_js(value: &DataValue, tm: &TypeMap) -> Result<JsValue, JsValue> {
match value {
DataValue::BoolTrue => Ok(JsValue::TRUE),
DataValue::BoolFalse => Ok(JsValue::FALSE),
@ -59,17 +87,57 @@ fn data_value_to_js(value: &DataValue, tm: &TypeMap) -> Result<JsValue, JsValue>
}
Ok(obj.into())
}
DataValue::EncryptedContainer(bytes)
| DataValue::SignedContainer(bytes)
| DataValue::SignedEncryptedContainer(bytes) => {
Ok(js_sys::Uint8Array::from(&bytes[..]).into())
DataValue::Encrypted(encrypted) => {
let obj = js_sys::Object::new();
set_prop(&obj, "kind", &JsValue::from_str("encrypted"))?;
set_prop(
&obj,
"encryptionType",
&JsValue::from_f64(encrypted.encryption_type.to_byte() as f64),
)?;
set_prop(
&obj,
"purpose",
&JsValue::from_f64(encrypted.purpose as f64),
)?;
set_prop(
&obj,
"recipientCount",
&JsValue::from_f64(encrypted.recipients.len() as f64),
)?;
let encoded = value
.to_bytes()
.map_err(|e| js_error(format!("encode protected value: {e}")))?;
set_prop(
&obj,
"encoded",
&js_sys::Uint8Array::from(&encoded[..]).into(),
)?;
Ok(obj.into())
}
DataValue::Signed(signed) => {
let obj = js_sys::Object::new();
set_prop(&obj, "kind", &JsValue::from_str("signed"))?;
set_prop(
&obj,
"signatureType",
&JsValue::from_f64(signed.algorithm as f64),
)?;
set_prop(&obj, "purpose", &JsValue::from_f64(signed.purpose as f64))?;
set_prop(
&obj,
"signerId",
&JsValue::bigint_from_str(&signed.signer_id.to_string()),
)?;
set_prop(&obj, "value", &data_value_to_js(&signed.value, tm)?)?;
Ok(obj.into())
}
DataValue::Null => Ok(JsValue::NULL),
}
}
const MAX_SAFE_INT: f64 = 9007199254740991.0; // 2^53 - 1
fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
if value.is_null() || value.is_undefined() {
return Ok(DataValue::Null);
}
@ -91,18 +159,13 @@ fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue>
return Ok(DataValue::Array(values));
}
if let Some(v) = value.as_f64() {
if let Some(v) = value.as_f64() {
if v.is_finite()
&& v.fract() == 0.0
&& (-(MAX_SAFE_INT + 1.0)..=MAX_SAFE_INT).contains(&v)
{
if v >= 0.0 {
return Ok(DataValue::UnsignedNumber(v as u128));
} else {
return Ok(DataValue::SignedNumber(v as i128));
}
if v.is_finite() && v.fract() == 0.0 && (-(MAX_SAFE_INT + 1.0)..=MAX_SAFE_INT).contains(&v)
{
if v >= 0.0 {
return Ok(DataValue::UnsignedNumber(v as u128));
} else {
return Ok(DataValue::SignedNumber(v as i128));
}
return Ok(DataValue::Float(v));
}
return Ok(DataValue::Float(v));
}
@ -115,10 +178,16 @@ fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue>
.as_string()
.ok_or_else(|| js_error("failed to stringify bigint"))?;
if let Some(unsigned) = as_string.strip_prefix('-') {
let n = unsigned
.parse::<i128>()
let magnitude = unsigned
.parse::<u128>()
.map_err(|_| js_error("bigint out of range"))?;
return Ok(DataValue::SignedNumber(-n));
if magnitude > (1u128 << 127) {
return Err(js_error("bigint out of range"));
}
if magnitude == (1u128 << 127) {
return Ok(DataValue::SignedNumber(i128::MIN));
}
return Ok(DataValue::SignedNumber(-(magnitude as i128)));
}
let n = as_string
.parse::<u128>()
@ -159,7 +228,11 @@ fn option_u32(options: &JsValue, key: &str) -> Result<Option<u32>, JsValue> {
let Some(n) = value.as_f64() else {
return Err(js_error(format!("{key} must be a number")));
};
Ok(Some(n as u32))
if !n.is_finite() || n.fract() != 0.0 || !(0.0..=MAX_SAFE_INT).contains(&n) {
return Err(js_error(format!("{key} must be an exact integer")));
}
let n = u32::try_from(n as u64).map_err(|_| js_error(format!("{key} out of range")))?;
Ok(Some(n))
}
fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
@ -168,6 +241,11 @@ fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
return Ok(None);
}
if let Some(n) = value.as_f64() {
if !n.is_finite() || n.fract() != 0.0 || !(0.0..=MAX_SAFE_INT).contains(&n) {
return Err(js_error(format!(
"{key} must be an exact integer number at most 2^53-1 or a bigint"
)));
}
return Ok(Some(n as u64));
}
let type_name = value.js_typeof().as_string().unwrap_or_default();
@ -185,18 +263,39 @@ fn option_u64(options: &JsValue, key: &str) -> Result<Option<u64>, JsValue> {
Err(js_error(format!("{key} must be a number or bigint")))
}
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes(frame)
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let tm = comm
.type_map()
.cloned()
.unwrap_or_else(|| TypeMap::new(PROTOCOL_VERSION));
let obj = js_sys::Object::new();
let data = js_sys::Object::new();
fn apply_frame_options(
mut message: CommunicationValue,
options: &JsValue,
) -> Result<CommunicationValue, JsValue> {
if !options.is_null() && !options.is_undefined() {
if let Some(id) = option_u32(options, "id")? {
message = message.with_id(id);
}
if let Some(sender) = option_u64(options, "sender")? {
message = message.with_sender(sender);
}
if let Some(receiver) = option_u64(options, "receiver")? {
message = message.with_receiver(receiver);
}
}
Ok(message)
}
if comm.get_id() != 0 {
set_prop(&obj, "id", &JsValue::from_f64(comm.get_id() as f64))?;
pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
parse_frame_value_with_type_map(frame, &TypeMap::latest())
}
pub(crate) fn parse_frame_value_with_type_map(
frame: &[u8],
type_map: &TypeMap,
) -> Result<JsValue, JsValue> {
let comm = CommunicationValue::from_bytes_with(frame, type_map)
.map_err(|e| js_error(format!("parse failed: {}", e)))?;
let tm = type_map;
let obj = js_sys::Object::new();
if let Some(id) = comm.id() {
set_prop(&obj, "id", &JsValue::from_f64(id as f64))?;
}
let frame_type = tm
@ -205,29 +304,22 @@ pub(crate) fn parse_frame_value(frame: &[u8]) -> Result<JsValue, JsValue> {
.unwrap_or_else(|| comm.get_type().0.to_string());
set_prop(&obj, "type", &JsValue::from_str(&frame_type))?;
if comm.get_sender() != 0 {
if let Some(sender) = comm.sender() {
set_prop(
&obj,
"sender",
&JsValue::bigint_from_str(&comm.get_sender().to_string()),
&JsValue::bigint_from_str(&sender.to_string()),
)?;
}
if comm.get_receiver() != 0 {
if let Some(receiver) = comm.receiver() {
set_prop(
&obj,
"receiver",
&JsValue::bigint_from_str(&comm.get_receiver().to_string()),
&JsValue::bigint_from_str(&receiver.to_string()),
)?;
}
for (key, value) in comm.data() {
let name = tm
.data_type_name(key.0)
.map(str::to_string)
.unwrap_or_else(|| key.0.to_string());
set_prop(&data, &name, &data_value_to_js(value, &tm)?)?;
}
set_prop(&obj, "data", &data.into())?;
set_prop(&obj, "data", &data_value_to_js(comm.payload(), &tm)?)?;
set_prop(&obj, "raw", &js_sys::Uint8Array::from(frame).into())?;
Ok(obj.into())
@ -266,25 +358,30 @@ 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 connected = matches!(comm.get_data(DataType::Connected), DataValue::BoolTrue);
let connected = matches!(
comm.get_data(DataType::Connected),
Some(DataValue::BoolTrue)
);
let client_nonce = match comm.get_data(DataType::ClientNonce) {
DataValue::UnsignedNumber(n) => Some(*n),
Some(DataValue::UnsignedNumber(n)) => Some(*n),
_ => None,
};
let assigned_id = match comm.get_data(DataType::Id) {
DataValue::UnsignedNumber(n) => Some(*n as u64),
Some(DataValue::UnsignedNumber(n)) => {
Some(u64::try_from(*n).map_err(|_| js_error("assigned ID is out of range"))?)
}
_ => None,
};
let timestamp = match comm.get_data(DataType::Timestamp) {
DataValue::UnsignedNumber(n) => Some(*n),
Some(DataValue::UnsignedNumber(n)) => Some(*n),
_ => None,
};
let signature = match comm.get_data(DataType::Signature) {
DataValue::Bytes(b) => Some(b.clone()),
Some(DataValue::Bytes(b)) => Some(b.clone()),
_ => None,
};
@ -332,6 +429,25 @@ pub fn parse_frame(frame: &[u8]) -> Result<JsValue, JsValue> {
parse_frame_value(frame)
}
/// 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"))?;
let tm = TypeMap::new(PROTOCOL_VERSION);
data_value_to_js(&value, &tm)
}
/// Encode one standalone `DataValue` using the negotiated/current type map.
#[wasm_bindgen]
pub fn encode_data_value(value: JsValue) -> Result<Vec<u8>, JsValue> {
let tm = TypeMap::new(PROTOCOL_VERSION);
js_to_data_value(&value, &tm)?
.to_bytes()
.map_err(|e| js_error(format!("encode data value failed: {e}")))
}
/// Build a typed MTP frame using generated communication/data type names.
#[wasm_bindgen]
pub fn build_frame(
@ -342,19 +458,7 @@ pub fn build_frame(
let comm_type = CommunicationType::from_name(message_type)
.ok_or_else(|| js_error(format!("unknown communication type: {message_type}")))?;
let tm = TypeMap::new(PROTOCOL_VERSION);
let mut msg = CommunicationValue::new(comm_type);
if !options.is_null() && !options.is_undefined() {
if let Some(id) = option_u32(&options, "id")? {
msg = msg.with_id(id);
}
if let Some(sender) = option_u64(&options, "sender")? {
msg = msg.with_sender(sender);
}
if let Some(receiver) = option_u64(&options, "receiver")? {
msg = msg.with_receiver(receiver);
}
}
let mut msg = apply_frame_options(CommunicationValue::new(comm_type), &options)?;
if data.is_object() && !js_sys::Uint8Array::instanceof(&data) && !js_sys::Array::is_array(&data)
{
@ -373,7 +477,9 @@ pub fn build_frame(
tm.version
))
})?;
msg = msg.add_data(id, js_to_data_value(&value, &tm)?);
msg = msg
.add_data(id, js_to_data_value(&value, &tm)?)
.map_err(|e| js_error(format!("add data failed: {e}")))?;
}
} else if !data.is_null() && !data.is_undefined() {
return Err(js_error(
@ -385,10 +491,37 @@ pub fn build_frame(
.map_err(|e| js_error(format!("encode failed: {}", e)))
}
/// Build a typed MTP frame around a complete serialized `DataValue` payload.
///
/// Unlike [`build_frame`], this does not interpret the payload as a clear data
/// container. It can therefore carry any value supported by the codec,
/// including signed and encrypted protection wrappers.
#[wasm_bindgen]
pub fn build_frame_with_payload(
message_type: &str,
serialized_payload: &[u8],
options: JsValue,
) -> 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 message =
apply_frame_options(CommunicationValue::new(comm_type), &options)?.with_payload(payload);
message
.to_bytes()
.map_err(|e| js_error(format!("encode failed: {e}")))
}
#[cfg(test)]
#[cfg(target_arch = "wasm32")]
mod tests {
use super::*;
use mtp_codec::ProtectionPurpose;
use mtp_crypto::{
Ed25519Signer, HybridKem, PublicKeyBundle, SignaturePqPublicKey, SignaturePublicKey,
};
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
@ -401,14 +534,14 @@ mod tests {
cv.get_type(),
CommunicationType::Ping.try_to_id(&tm).unwrap()
);
assert_eq!(cv.get_sender(), 42);
assert_eq!(cv.sender(), Some(42));
assert_eq!(
cv.get_data(DataType::Description),
&DataValue::Str("test-ping".into())
Some(&DataValue::Str("test-ping".into()))
);
assert_eq!(
cv.get_data(DataType::Timestamp),
&DataValue::UnsignedNumber(1234567890)
Some(&DataValue::UnsignedNumber(1234567890))
);
}
@ -423,18 +556,18 @@ mod tests {
cv.get_type(),
CommunicationType::Ping.try_to_id(&tm).unwrap()
);
assert_eq!(cv.get_sender(), 99);
assert_eq!(cv.sender(), Some(99));
assert_eq!(
cv.get_data(DataType::Description),
&DataValue::Str("with-data".into())
Some(&DataValue::Str("with-data".into()))
);
assert_eq!(
cv.get_data(DataType::Timestamp),
&DataValue::UnsignedNumber(555)
Some(&DataValue::UnsignedNumber(555))
);
assert_eq!(
cv.get_data(DataType::Id),
&DataValue::Bytes(payload.to_vec())
Some(&DataValue::Bytes(payload.to_vec()))
);
}
@ -442,7 +575,273 @@ mod tests {
fn build_ping_frame_client_id_zero() {
let bytes = build_ping_frame(0, "zero-id", 0, &[]).expect("encode failed");
let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed");
assert_eq!(cv.get_sender(), 0);
assert_eq!(cv.sender(), Some(0));
}
#[wasm_bindgen_test]
fn parse_frame_preserves_a_generic_payload() {
let bytes = CommunicationValue::new(CommunicationType::Pong)
.with_payload(DataValue::Bytes(vec![1, 2, 3]))
.to_bytes()
.expect("encode failed");
let parsed = parse_frame_value(&bytes).expect("parse failed");
let data = js_sys::Reflect::get(&parsed, &JsValue::from_str("data"))
.expect("data should be present");
assert_eq!(js_sys::Uint8Array::new(&data).to_vec(), vec![1, 2, 3]);
}
#[wasm_bindgen_test]
fn integer_data_values_round_trip_without_losing_numeric_type() {
let tm = TypeMap::latest();
let cases = [
(DataValue::UnsignedNumber(9_007_199_254_740_991), "number"),
(DataValue::UnsignedNumber(9_007_199_254_740_992), "bigint"),
(DataValue::SignedNumber(-9_007_199_254_740_992), "bigint"),
(DataValue::SignedNumber(i128::MIN), "bigint"),
(DataValue::UnsignedNumber(u128::from(u64::MAX)), "bigint"),
];
for (original, expected_type) in cases {
let javascript = data_value_to_js(&original, &tm).expect("decode value");
assert_eq!(
javascript.js_typeof().as_string().as_deref(),
Some(expected_type)
);
assert_eq!(
js_to_data_value(&javascript, &tm).expect("encode value"),
original
);
}
}
#[wasm_bindgen_test]
fn parse_frame_preserves_signed_value_structure() {
let (signer, _secret_key, _public_key) = Ed25519Signer::generate();
let signed = DataValue::Str("signed payload".into())
.sign(0xfeed_beef, ProtectionPurpose::from(7), &signer)
.expect("signing failed");
let bytes = CommunicationValue::new(CommunicationType::Pong)
.with_payload(signed)
.to_bytes()
.expect("encode failed");
let parsed = parse_frame_value(&bytes).expect("parse failed");
let data = js_sys::Reflect::get(&parsed, &"data".into()).expect("data should be present");
assert_eq!(
js_sys::Reflect::get(&data, &"kind".into())
.expect("kind should be present")
.as_string()
.as_deref(),
Some("signed")
);
assert_eq!(
js_sys::Reflect::get(&data, &"purpose".into())
.expect("purpose should be present")
.as_f64(),
Some(7.0)
);
let signer_id = js_sys::Reflect::get(&data, &"signerId".into())
.expect("signerId should be present")
.unchecked_into::<js_sys::BigInt>()
.to_string(10)
.expect("signerId should stringify")
.as_string();
assert_eq!(signer_id.as_deref(), Some("4276993775"));
assert_eq!(
js_sys::Reflect::get(&data, &"value".into())
.expect("value should be present")
.as_string()
.as_deref(),
Some("signed payload")
);
}
#[wasm_bindgen_test]
fn parse_frame_keeps_encrypted_contents_private() {
let (_secret_key, public_key) = HybridKem::generate_keypair();
let recipient = PublicKeyBundle::new(
public_key,
SignaturePqPublicKey::new(Vec::new()),
SignaturePublicKey::new(Vec::new()),
);
let encrypted = DataValue::Str("secret payload".into())
.encrypt_for(&[recipient], ProtectionPurpose::from(9))
.expect("encryption failed");
let encoded = encrypted.to_bytes().expect("encode protected value failed");
let bytes = CommunicationValue::new(CommunicationType::Pong)
.with_payload(encrypted)
.to_bytes()
.expect("encode failed");
let parsed = parse_frame_value(&bytes).expect("parse failed");
let data = js_sys::Reflect::get(&parsed, &"data".into()).expect("data should be present");
assert_eq!(
js_sys::Reflect::get(&data, &"kind".into())
.expect("kind should be present")
.as_string()
.as_deref(),
Some("encrypted")
);
assert_eq!(
js_sys::Reflect::get(&data, &"recipientCount".into())
.expect("recipientCount should be present")
.as_f64(),
Some(1.0)
);
assert!(!js_sys::Reflect::has(&data, &"value".into()).unwrap_or(false));
assert_eq!(
js_sys::Reflect::get(&data, &"encoded".into())
.expect("encoded should be present")
.unchecked_into::<js_sys::Uint8Array>()
.to_vec(),
encoded
);
}
#[wasm_bindgen_test]
fn parse_frame_preserves_signed_encrypted_composition() {
let (signer, _secret_key, _public_key) = Ed25519Signer::generate();
let (_kem_secret_key, kem_public_key) = HybridKem::generate_keypair();
let recipient = PublicKeyBundle::new(
kem_public_key,
SignaturePqPublicKey::new(Vec::new()),
SignaturePublicKey::new(Vec::new()),
);
let encrypted = DataValue::Container(vec![(
mtp_type_map::DataTypeId(32),
DataValue::Str("secret payload".into()),
)])
.encrypt_for(&[recipient], ProtectionPurpose::from(9))
.expect("encryption failed");
let encrypted_bytes = encrypted.to_bytes().expect("encrypted value should encode");
let signed = encrypted
.sign(0x0102_0304_0506_0708, ProtectionPurpose::from(7), &signer)
.expect("signing failed");
let frame = CommunicationValue::new(CommunicationType::Pong)
.with_payload(signed)
.to_bytes()
.expect("frame should encode");
let parsed = parse_frame_value(&frame).expect("frame should parse");
let signed = js_sys::Reflect::get(&parsed, &"data".into())
.expect("signed payload should be present");
assert_eq!(
js_sys::Reflect::get(&signed, &"kind".into())
.expect("signed kind should be present")
.as_string()
.as_deref(),
Some("signed")
);
let encrypted = js_sys::Reflect::get(&signed, &"value".into())
.expect("encrypted inner value should be present");
assert_eq!(
js_sys::Reflect::get(&encrypted, &"kind".into())
.expect("encrypted kind should be present")
.as_string()
.as_deref(),
Some("encrypted")
);
assert_eq!(
js_sys::Reflect::get(&encrypted, &"encoded".into())
.expect("encrypted encoding should be present")
.unchecked_into::<js_sys::Uint8Array>()
.to_vec(),
encrypted_bytes
);
}
#[wasm_bindgen_test]
fn frame_ids_use_bigints_without_lossy_number_casts() {
let options = js_sys::Object::new();
js_sys::Reflect::set(
&options,
&"sender".into(),
&JsValue::bigint_from_str("18446744073709551615"),
)
.expect("sender option should be set");
let bytes = build_frame("Pong", JsValue::NULL, options.into()).expect("build failed");
let frame = CommunicationValue::from_bytes(&bytes).expect("decode failed");
assert_eq!(frame.sender(), Some(u64::MAX));
let unsafe_number = js_sys::Object::new();
js_sys::Reflect::set(
&unsafe_number,
&"sender".into(),
&JsValue::from_f64(MAX_SAFE_INT + 1.0),
)
.expect("sender option should be set");
assert!(build_frame("Pong", JsValue::NULL, unsafe_number.into()).is_err());
}
#[wasm_bindgen_test]
fn build_frame_with_payload_preserves_clear_and_protected_payloads() {
let (signer, _secret_key, _public_key) = Ed25519Signer::generate();
let (_kem_secret_key, kem_public_key) = HybridKem::generate_keypair();
let recipient = PublicKeyBundle::new(
kem_public_key,
SignaturePqPublicKey::new(Vec::new()),
SignaturePublicKey::new(Vec::new()),
);
let clear = DataValue::Str("generic protected payload".into());
let signed = clear
.clone()
.sign(0x0102_0304_0506_0708, ProtectionPurpose::from(7), &signer)
.expect("signing failed");
let encrypted = clear
.clone()
.encrypt_for(&[recipient.clone()], ProtectionPurpose::from(9))
.expect("encryption failed");
let signed_encrypted = signed
.clone()
.encrypt_for(&[recipient], ProtectionPurpose::from(9))
.expect("signed encryption failed");
for payload in [clear, signed, encrypted, signed_encrypted] {
let serialized = payload.to_bytes().expect("payload encoding failed");
let frame = build_frame_with_payload("Pong", &serialized, JsValue::NULL)
.expect("frame encoding failed");
let decoded = CommunicationValue::from_bytes(&frame).expect("frame decoding failed");
assert_eq!(
decoded
.payload()
.to_bytes()
.expect("payload re-encoding failed"),
serialized
);
}
}
#[wasm_bindgen_test]
fn build_frame_with_payload_applies_frame_options() {
let payload = DataValue::Str("payload".into())
.to_bytes()
.expect("payload encoding failed");
let options = js_sys::Object::new();
js_sys::Reflect::set(&options, &"id".into(), &JsValue::from_f64(17.0))
.expect("id option should be set");
js_sys::Reflect::set(
&options,
&"sender".into(),
&JsValue::bigint_from_str("18446744073709551615"),
)
.expect("sender option should be set");
js_sys::Reflect::set(&options, &"receiver".into(), &JsValue::from_f64(23.0))
.expect("receiver option should be set");
let frame = build_frame_with_payload("Pong", &payload, options.into())
.expect("frame encoding failed");
let decoded = CommunicationValue::from_bytes(&frame).expect("frame decoding failed");
assert_eq!(decoded.id(), Some(17));
assert_eq!(decoded.sender(), Some(u64::MAX));
assert_eq!(decoded.receiver(), Some(23));
assert_eq!(
decoded
.payload()
.to_bytes()
.expect("payload re-encoding failed"),
payload
);
}
#[wasm_bindgen_test]