Merge
Crypto WASM TESTS
This commit is contained in:
parent
2a00bb35e7
commit
687e6f9642
49 changed files with 6272 additions and 366 deletions
|
|
@ -1,5 +1,6 @@
|
|||
# Default to the wasm32 target when running cargo from inside this crate dir.
|
||||
# The web_sys_unstable_apis cfg lives in the workspace-root .cargo/config.toml,
|
||||
# scoped to [target.wasm32-unknown-unknown], so it applies here too (the root
|
||||
# config is an ancestor) and to `-p mtp-wasm` builds invoked from the root.
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||
|
|
|
|||
|
|
@ -3,16 +3,80 @@ use std::rc::Rc;
|
|||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_codec::{
|
||||
CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION,
|
||||
};
|
||||
use mtp_type_map::{CommunicationTypeId, DataTypeId};
|
||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue, PROTOCOL_VERSION};
|
||||
use mtp_type_map::CommunicationTypeId;
|
||||
|
||||
use mtp_crypto::SignatureScheme;
|
||||
|
||||
use crate::error::js_error;
|
||||
use crate::transport::WasmTransport;
|
||||
|
||||
fn raw_frame_preview(bytes: &[u8]) -> String {
|
||||
let shown = bytes.len().min(256);
|
||||
let mut preview = hex::encode(&bytes[..shown]);
|
||||
if bytes.len() > shown {
|
||||
preview.push_str("...");
|
||||
}
|
||||
format!("{} bytes, hex={preview}", bytes.len())
|
||||
}
|
||||
|
||||
fn unexpected_response_type_error(
|
||||
context: &str,
|
||||
expected_type: CommunicationTypeId,
|
||||
response_type: CommunicationTypeId,
|
||||
response: &[u8],
|
||||
parsed: &CommunicationValue,
|
||||
) -> JsValue {
|
||||
js_error(&format!(
|
||||
"unexpected response type during {context}: expected {:?}, got {:?}; raw {}; parsed {}",
|
||||
expected_type,
|
||||
response_type,
|
||||
raw_frame_preview(response),
|
||||
parsed
|
||||
))
|
||||
}
|
||||
|
||||
/// Verify the host's authentication signature over the handshake response,
|
||||
/// mirroring the native client (`client/src/lib.rs`). The signed payload is
|
||||
/// `0x01 || id.to_be_bytes() || client_nonce.to_be_bytes() || host_nonce.to_be_bytes()`,
|
||||
/// where `id` is the client id for login and the host-assigned id for register.
|
||||
/// The Ed25519 signature is mandatory; the ML-DSA signature is verified only
|
||||
/// when the host included one.
|
||||
fn verify_host_signature(
|
||||
resp: &CommunicationValue,
|
||||
tm: &mtp_codec::TypeMap,
|
||||
host_pk: &mtp_crypto::PublicKeyBundle,
|
||||
id: u64,
|
||||
client_nonce: u128,
|
||||
) -> Result<(), JsValue> {
|
||||
let host_new_nonce = match resp.get_data(DataType::Timestamp.to_id(tm)) {
|
||||
DataValue::UnsignedNumber(n) => *n,
|
||||
_ => return Err(js_error("missing host nonce")),
|
||||
};
|
||||
let host_sig = match resp.get_data(DataType::Signature.to_id(tm)) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => return Err(js_error("missing host signature")),
|
||||
};
|
||||
let host_pq_sig = match resp.get_data(DataType::PqSignature.to_id(tm)) {
|
||||
DataValue::Bytes(b) => b.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
let mut payload = Vec::new();
|
||||
payload.push(0x01);
|
||||
payload.extend_from_slice(&id.to_be_bytes());
|
||||
payload.extend_from_slice(&client_nonce.to_be_bytes());
|
||||
payload.extend_from_slice(&host_new_nonce.to_be_bytes());
|
||||
|
||||
mtp_crypto::verify_ed25519(&host_pk.sig_cl_public_key, &payload, &host_sig)
|
||||
.map_err(|_| js_error("host signature invalid"))?;
|
||||
if !host_pq_sig.is_empty() {
|
||||
mtp_crypto::verify_ml_dsa(&host_pk.sig_pq_public_key, &payload, &host_pq_sig)
|
||||
.map_err(|_| js_error("host PQ signature invalid"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
|
|
@ -33,17 +97,27 @@ pub struct ConnectionConfig {
|
|||
impl ConnectionConfig {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(url: String) -> Self {
|
||||
Self { url, server_certificate_hashes: None, client_id: 0 }
|
||||
Self {
|
||||
url,
|
||||
server_certificate_hashes: None,
|
||||
client_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn url(&self) -> String { self.url.clone() }
|
||||
pub fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_client_id(&mut self, id: u64) { self.client_id = id; }
|
||||
pub fn set_client_id(&mut self, id: u64) {
|
||||
self.client_id = id;
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn client_id(&self) -> u64 { self.client_id }
|
||||
pub fn client_id(&self) -> u64 {
|
||||
self.client_id
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
|
||||
|
|
@ -79,24 +153,29 @@ impl WasmClient {
|
|||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_supported() -> bool {
|
||||
js_sys::Reflect::has(&js_sys::global(), &JsValue::from_str("WebTransport"))
|
||||
.unwrap_or(false)
|
||||
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 }
|
||||
pub fn state(&self) -> u8 {
|
||||
self.state.get() as u8
|
||||
}
|
||||
|
||||
/// Unauthenticated connect (sends basic Identification, enables receive loop).
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect(&mut self, config: &ConnectionConfig) -> Result<(), JsValue> {
|
||||
self.set_state(ConnectionState::Connecting);
|
||||
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||
let transport =
|
||||
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||
let inner = transport.inner().clone();
|
||||
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
let ident = CommunicationValue::new(CommunicationType::Identification)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(config.client_id as u128));
|
||||
.add_typed_default(
|
||||
DataType::Id,
|
||||
DataValue::UnsignedNumber(config.client_id as u128),
|
||||
);
|
||||
let ident_bytes = ident
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
|
|
@ -109,7 +188,9 @@ impl WasmClient {
|
|||
let on_msg = self.on_message.clone();
|
||||
let on_err = self.on_error.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
WasmTransport::from_inner(inner).receive_loop(on_msg, on_err).await;
|
||||
WasmTransport::from_inner(inner)
|
||||
.receive_loop(on_msg, on_err)
|
||||
.await;
|
||||
state.set(ConnectionState::Disconnected);
|
||||
});
|
||||
Ok(())
|
||||
|
|
@ -127,19 +208,20 @@ impl WasmClient {
|
|||
pub async fn auth_connect(
|
||||
&mut self,
|
||||
config: &ConnectionConfig,
|
||||
_host_public_key_bytes: &[u8],
|
||||
host_public_key_bytes: &[u8],
|
||||
keyring_bytes: &[u8],
|
||||
client_id: u64,
|
||||
) -> Result<u64, JsValue> {
|
||||
self.set_state(ConnectionState::Connecting);
|
||||
|
||||
let host_pk = mtp_crypto::PublicKeyBundle::from_bytes(host_public_key_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid host public key: {}", e)))?;
|
||||
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
||||
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
let mut nonce_bytes = [0u8; 16];
|
||||
getrandom::fill(&mut nonce_bytes)
|
||||
.map_err(|_| js_error("rng failed"))?;
|
||||
getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
|
||||
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
||||
|
||||
// Build signature payload: version || client_id || client_nonce
|
||||
|
|
@ -150,18 +232,23 @@ impl WasmClient {
|
|||
|
||||
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||
let signature = signer.sign(&sig_payload)
|
||||
let signature = signer
|
||||
.sign(&sig_payload)
|
||||
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
||||
|
||||
let frame = CommunicationValue::new(CommunicationType::Identification)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128))
|
||||
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce))
|
||||
.add_typed_default(
|
||||
DataType::ClientNonce,
|
||||
DataValue::UnsignedNumber(client_nonce),
|
||||
)
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
|
||||
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||
let transport =
|
||||
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||
let inner = transport.inner().clone();
|
||||
transport.send_frame(&frame).await?;
|
||||
|
||||
|
|
@ -170,26 +257,45 @@ impl WasmClient {
|
|||
let resp_comm = CommunicationValue::from_bytes(&response)
|
||||
.map_err(|e| js_error(&format!("parse response: {}", e)))?;
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
let resp_type = resp_comm.get_type();
|
||||
let expected_type = CommunicationTypeId(16); // IdentificationResponse
|
||||
let expected_type = CommunicationType::IdentificationResponse.to_id(&tm);
|
||||
if resp_type != expected_type {
|
||||
return Err(js_error("unexpected response type"));
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(unexpected_response_type_error(
|
||||
"auth_connect",
|
||||
expected_type,
|
||||
resp_type,
|
||||
&response,
|
||||
&resp_comm,
|
||||
));
|
||||
}
|
||||
|
||||
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
||||
if resp_comm.get_data(DataType::Connected.to_id(&tm)) != &DataValue::BoolTrue {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("host rejected authentication"));
|
||||
}
|
||||
|
||||
// Verify echoed nonce
|
||||
let echo_nonce = resp_comm.get_data(DataTypeId(7));
|
||||
let echo_nonce = resp_comm.get_data(DataType::ClientNonce.to_id(&tm));
|
||||
if *echo_nonce != DataValue::UnsignedNumber(client_nonce) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("nonce mismatch"));
|
||||
}
|
||||
|
||||
// Verify the host's signature over the handshake (login: id is client_id).
|
||||
if let Err(e) = verify_host_signature(&resp_comm, &tm, &host_pk, client_id, client_nonce) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Extract assigned ID
|
||||
let assigned_id = match resp_comm.get_data(DataTypeId(6)) {
|
||||
let assigned_id = match resp_comm.get_data(DataType::Id.to_id(&tm)) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => return Err(js_error("missing assigned ID")),
|
||||
_ => {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("missing assigned ID"));
|
||||
}
|
||||
};
|
||||
|
||||
self.transport = Some(transport);
|
||||
|
|
@ -199,7 +305,9 @@ impl WasmClient {
|
|||
let on_msg = self.on_message.clone();
|
||||
let on_err = self.on_error.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
WasmTransport::from_inner(inner).receive_loop(on_msg, on_err).await;
|
||||
WasmTransport::from_inner(inner)
|
||||
.receive_loop(on_msg, on_err)
|
||||
.await;
|
||||
state.set(ConnectionState::Disconnected);
|
||||
});
|
||||
|
||||
|
|
@ -222,15 +330,14 @@ impl WasmClient {
|
|||
) -> Result<u64, JsValue> {
|
||||
self.set_state(ConnectionState::Connecting);
|
||||
|
||||
let _host_pk = mtp_crypto::PublicKeyBundle::from_bytes(host_public_key_bytes)
|
||||
let host_pk = mtp_crypto::PublicKeyBundle::from_bytes(host_public_key_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid host public key: {}", e)))?;
|
||||
let keyring = mtp_crypto::Keyring::from_bytes(keyring_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
||||
|
||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||
let mut nonce_bytes = [0u8; 16];
|
||||
getrandom::fill(&mut nonce_bytes)
|
||||
.map_err(|_| js_error("rng failed"))?;
|
||||
getrandom::fill(&mut nonce_bytes).map_err(|_| js_error("rng failed"))?;
|
||||
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
||||
|
||||
let pk_bytes = keyring.public_key_bundle().as_bytes();
|
||||
|
|
@ -243,18 +350,23 @@ impl WasmClient {
|
|||
|
||||
let signer = mtp_crypto::Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||
let signature = signer.sign(&sig_payload)
|
||||
let signature = signer
|
||||
.sign(&sig_payload)
|
||||
.map_err(|e| js_error(&format!("signature failed: {}", e)))?;
|
||||
|
||||
let frame = CommunicationValue::new(CommunicationType::Register)
|
||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||
.add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(client_nonce))
|
||||
.add_typed_default(
|
||||
DataType::ClientNonce,
|
||||
DataValue::UnsignedNumber(client_nonce),
|
||||
)
|
||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes))
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||
.to_bytes()
|
||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||
|
||||
let transport = WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||
let transport =
|
||||
WasmTransport::connect(&config.url, config.server_certificate_hashes.clone()).await?;
|
||||
let inner = transport.inner().clone();
|
||||
transport.send_frame(&frame).await?;
|
||||
|
||||
|
|
@ -262,26 +374,47 @@ impl WasmClient {
|
|||
let resp_comm = CommunicationValue::from_bytes(&response)
|
||||
.map_err(|e| js_error(&format!("parse response: {}", e)))?;
|
||||
|
||||
let tm = mtp_codec::TypeMap::latest();
|
||||
let resp_type = resp_comm.get_type();
|
||||
let expected_type = CommunicationTypeId(18); // RegisterResponse
|
||||
let expected_type = CommunicationType::RegisterResponse.to_id(&tm);
|
||||
if resp_type != expected_type {
|
||||
return Err(js_error("unexpected response type"));
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(unexpected_response_type_error(
|
||||
"auth_register",
|
||||
expected_type,
|
||||
resp_type,
|
||||
&response,
|
||||
&resp_comm,
|
||||
));
|
||||
}
|
||||
|
||||
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
||||
if resp_comm.get_data(DataType::Connected.to_id(&tm)) != &DataValue::BoolTrue {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("host rejected registration"));
|
||||
}
|
||||
|
||||
let echo = resp_comm.get_data(DataTypeId(7));
|
||||
let echo = resp_comm.get_data(DataType::ClientNonce.to_id(&tm));
|
||||
if *echo != DataValue::UnsignedNumber(client_nonce) {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("nonce mismatch"));
|
||||
}
|
||||
|
||||
let assigned_id = match resp_comm.get_data(DataTypeId(6)) {
|
||||
let assigned_id = match resp_comm.get_data(DataType::Id.to_id(&tm)) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => return Err(js_error("missing assigned ID")),
|
||||
_ => {
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(js_error("missing assigned ID"));
|
||||
}
|
||||
};
|
||||
|
||||
// Verify the host's signature over the handshake (register: id is the
|
||||
// host-assigned id).
|
||||
if let Err(e) = verify_host_signature(&resp_comm, &tm, &host_pk, assigned_id, client_nonce)
|
||||
{
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.set_state(ConnectionState::Connected);
|
||||
|
||||
|
|
@ -289,7 +422,9 @@ impl WasmClient {
|
|||
let on_msg = self.on_message.clone();
|
||||
let on_err = self.on_error.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
WasmTransport::from_inner(inner).receive_loop(on_msg, on_err).await;
|
||||
WasmTransport::from_inner(inner)
|
||||
.receive_loop(on_msg, on_err)
|
||||
.await;
|
||||
state.set(ConnectionState::Disconnected);
|
||||
});
|
||||
|
||||
|
|
@ -306,16 +441,17 @@ impl WasmClient {
|
|||
|
||||
#[wasm_bindgen]
|
||||
pub fn disconnect(&mut self) {
|
||||
if let Some(t) = &self.transport { t.close(); }
|
||||
if let Some(t) = &self.transport {
|
||||
t.close();
|
||||
}
|
||||
self.transport = None;
|
||||
self.set_state(ConnectionState::Disconnected);
|
||||
}
|
||||
|
||||
fn set_state(&self, new_state: ConnectionState) {
|
||||
self.state.set(new_state);
|
||||
let _ = self.on_state_change.call1(
|
||||
&JsValue::NULL,
|
||||
&JsValue::from(new_state as u8),
|
||||
);
|
||||
let _ = self
|
||||
.on_state_change
|
||||
.call1(&JsValue::NULL, &JsValue::from(new_state as u8));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_crypto::{
|
||||
AeadDecrypt, AeadEncrypt, Ed25519Signer, KemPrivateKey, KemPublicKey, Keyring,
|
||||
PublicKeyBundle, SignaturePqPrivateKey, SignaturePqPublicKey, SignaturePrivateKey,
|
||||
SignaturePublicKey, SignatureScheme, ChaCha20Poly1305, sha256, sha256_double,
|
||||
AeadDecrypt, AeadEncrypt, ChaCha20Poly1305, Ed25519Signer, KemPrivateKey, KemPublicKey,
|
||||
Keyring, PublicKeyBundle, SignaturePqPrivateKey, SignaturePqPublicKey, SignaturePrivateKey,
|
||||
SignaturePublicKey, SignatureScheme, sha256, sha256_double,
|
||||
};
|
||||
|
||||
use crate::error::js_error;
|
||||
|
|
@ -28,8 +28,8 @@ impl WasmKeyring {
|
|||
/// Deserialise a keyring from bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<WasmKeyring, JsValue> {
|
||||
let inner =
|
||||
Keyring::from_bytes(bytes).map_err(|e| js_error(&format!("Keyring::from_bytes: {}", e)))?;
|
||||
let inner = Keyring::from_bytes(bytes)
|
||||
.map_err(|e| js_error(&format!("Keyring::from_bytes: {}", e)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +217,11 @@ pub fn ed25519_generate() -> Result<JsValue, JsValue> {
|
|||
|
||||
/// Standalone Ed25519 signature verification.
|
||||
#[wasm_bindgen]
|
||||
pub fn ed25519_verify(public_key: Vec<u8>, message: &[u8], signature: &[u8]) -> Result<(), JsValue> {
|
||||
pub fn ed25519_verify(
|
||||
public_key: Vec<u8>,
|
||||
message: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Result<(), JsValue> {
|
||||
let pk = SignaturePublicKey::new(public_key);
|
||||
mtp_crypto::verify_ed25519(&pk, message, signature)
|
||||
.map_err(|e| js_error(&format!("verify_ed25519 failed: {}", e)))
|
||||
|
|
@ -245,7 +249,12 @@ pub fn wasm_sha256_double(data: &[u8]) -> Vec<u8> {
|
|||
|
||||
/// HKDF-expand: derive `len` bytes from `ikm` with `salt` and `info`.
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_hkdf_expand(ikm: &[u8], salt: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>, JsValue> {
|
||||
pub fn wasm_hkdf_expand(
|
||||
ikm: &[u8],
|
||||
salt: &[u8],
|
||||
info: &[u8],
|
||||
len: usize,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
mtp_crypto::hkdf_expand(ikm, salt, info, len)
|
||||
.map_err(|e| js_error(&format!("hkdf_expand failed: {}", e)))
|
||||
}
|
||||
|
|
@ -416,16 +425,18 @@ mod tests {
|
|||
fn sha256_empty() {
|
||||
let result = wasm_sha256(b"");
|
||||
// SHA-256 of empty string
|
||||
let expected = hex::decode("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
.expect("hex decode");
|
||||
let expected =
|
||||
hex::decode("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
.expect("hex decode");
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn sha256_hello() {
|
||||
let result = wasm_sha256(b"hello");
|
||||
let expected = hex::decode("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
|
||||
.expect("hex decode");
|
||||
let expected =
|
||||
hex::decode("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
|
||||
.expect("hex decode");
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
|
|
@ -443,8 +454,7 @@ mod tests {
|
|||
|
||||
#[wasm_bindgen_test]
|
||||
fn hkdf_expand_produces_correct_length() {
|
||||
let result = wasm_hkdf_expand(b"ikm", b"salt", b"info", 32)
|
||||
.expect("hkdf_expand failed");
|
||||
let result = wasm_hkdf_expand(b"ikm", b"salt", b"info", 32).expect("hkdf_expand failed");
|
||||
assert_eq!(result.len(), 32);
|
||||
}
|
||||
|
||||
|
|
@ -457,22 +467,21 @@ mod tests {
|
|||
|
||||
#[wasm_bindgen_test]
|
||||
fn derive_encryption_key_roundtrip() {
|
||||
let key = wasm_derive_encryption_key(b"password", b"salt", b"context")
|
||||
.expect("derive failed");
|
||||
let key =
|
||||
wasm_derive_encryption_key(b"password", b"salt", b"context").expect("derive failed");
|
||||
assert_eq!(key.len(), 32);
|
||||
|
||||
// Deterministic: same inputs = same key
|
||||
let key2 = wasm_derive_encryption_key(b"password", b"salt", b"context")
|
||||
.expect("derive failed");
|
||||
let key2 =
|
||||
wasm_derive_encryption_key(b"password", b"salt", b"context").expect("derive failed");
|
||||
assert_eq!(key, key2);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn derive_encryption_key_different_inputs_different_key() {
|
||||
let key = wasm_derive_encryption_key(b"pass1", b"salt", b"context")
|
||||
.expect("derive failed");
|
||||
let key2 = wasm_derive_encryption_key(b"pass2", b"salt", b"context")
|
||||
.expect("derive failed");
|
||||
let key = wasm_derive_encryption_key(b"pass1", b"salt", b"context").expect("derive failed");
|
||||
let key2 =
|
||||
wasm_derive_encryption_key(b"pass2", b"salt", b"context").expect("derive failed");
|
||||
assert_ne!(key, key2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_codec::{
|
||||
CommunicationType, CommunicationTypeId, CommunicationValue, DataType, DataTypeId, DataValue,
|
||||
};
|
||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue};
|
||||
use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, PublicKeyBundle, SigAlgorithm};
|
||||
use mtp_type_map::communication_type_name;
|
||||
use mtp_crypto::{Ed25519Signer, EncryptionType, Keyring, SigAlgorithm};
|
||||
|
||||
use crate::error::js_error;
|
||||
|
||||
|
|
@ -17,8 +15,14 @@ pub fn build_ping_frame(
|
|||
data: &[u8],
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let mut msg = CommunicationValue::new(CommunicationType::Ping)
|
||||
.add_typed_default(DataType::Description, DataValue::Str(description.to_string()))
|
||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128))
|
||||
.add_typed_default(
|
||||
DataType::Description,
|
||||
DataValue::Str(description.to_string()),
|
||||
)
|
||||
.add_typed_default(
|
||||
DataType::Timestamp,
|
||||
DataValue::UnsignedNumber(timestamp as u128),
|
||||
)
|
||||
.with_sender(client_id);
|
||||
|
||||
if !data.is_empty() {
|
||||
|
|
@ -32,13 +36,18 @@ pub fn build_ping_frame(
|
|||
/// Build a demo Ping frame with encrypted and signed containers
|
||||
/// (mirrors the Rust client example but uses only reserved data types).
|
||||
#[wasm_bindgen]
|
||||
pub fn build_demo_message(client_id: u64, keyring_bytes: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
pub fn build_demo_message(
|
||||
client_id: u64,
|
||||
keyring_bytes: &[u8],
|
||||
host_bundle_bytes: &[u8],
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
let keyring = Keyring::from_bytes(keyring_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
||||
|
||||
// Demo encrypts to its own KEM public key (encrypt-to-self) so the roundtrip
|
||||
// is self-contained; a real client would encrypt to the server's bundle.
|
||||
let recipient = keyring.public_key_bundle();
|
||||
// Encrypt to the server's KEM public key; the server decrypts with its keyring.
|
||||
// (The client keyring only needs the Ed25519 signing key for this demo.)
|
||||
let recipient = PublicKeyBundle::from_bytes(host_bundle_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid host bundle: {}", e)))?;
|
||||
let enc_type = EncryptionType::MlKemChaCha20Poly1305;
|
||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||
|
|
@ -49,7 +58,8 @@ pub fn build_demo_message(client_id: u64, keyring_bytes: &[u8]) -> Result<Vec<u8
|
|||
(DataTypeId(2), DataValue::UnsignedNumber(42)),
|
||||
]);
|
||||
let mut dv_enc = inner_enc;
|
||||
dv_enc.encrypt_container(enc_type, &recipient, b"demo-aad")
|
||||
dv_enc
|
||||
.encrypt_container(enc_type, &recipient, b"demo-aad")
|
||||
.ok_or_else(|| js_error("encryption failed"))?;
|
||||
|
||||
// Signed container
|
||||
|
|
@ -58,23 +68,40 @@ pub fn build_demo_message(client_id: u64, keyring_bytes: &[u8]) -> Result<Vec<u8
|
|||
(DataTypeId(2), DataValue::UnsignedNumber(99)),
|
||||
]);
|
||||
let mut dv_sig = inner_sig;
|
||||
dv_sig.sign_container(SigAlgorithm::ED25519, &signer)
|
||||
dv_sig
|
||||
.sign_container(SigAlgorithm::ED25519, &signer)
|
||||
.ok_or_else(|| js_error("signing failed"))?;
|
||||
|
||||
// Signed + encrypted container
|
||||
let inner_sec = DataValue::Container(vec![
|
||||
(DataTypeId(1), DataValue::Str("signed+encrypted payload".into())),
|
||||
(
|
||||
DataTypeId(1),
|
||||
DataValue::Str("signed+encrypted payload".into()),
|
||||
),
|
||||
(DataTypeId(2), DataValue::UnsignedNumber(7)),
|
||||
]);
|
||||
let mut dv_sec = inner_sec;
|
||||
dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, enc_type, &recipient, b"demo-aad")
|
||||
dv_sec
|
||||
.sign_and_encrypt_container(
|
||||
SigAlgorithm::ED25519,
|
||||
&signer,
|
||||
enc_type,
|
||||
&recipient,
|
||||
b"demo-aad",
|
||||
)
|
||||
.ok_or_else(|| js_error("sign+encrypt failed"))?;
|
||||
|
||||
let timestamp = js_sys::Date::now() as u64;
|
||||
|
||||
let msg = CommunicationValue::new(CommunicationType::Ping)
|
||||
.add_typed_default(DataType::Description, DataValue::Str("MTP WASM Demo".into()))
|
||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128))
|
||||
.add_typed_default(
|
||||
DataType::Description,
|
||||
DataValue::Str("MTP WASM Demo".into()),
|
||||
)
|
||||
.add_typed_default(
|
||||
DataType::Timestamp,
|
||||
DataValue::UnsignedNumber(timestamp as u128),
|
||||
)
|
||||
.add_typed_default(DataType::Version, DataValue::Str("demo-wasm".into()))
|
||||
.with_sender(client_id);
|
||||
|
||||
|
|
@ -170,7 +197,11 @@ pub fn parse_response_frame(frame: &[u8]) -> Result<String, JsValue> {
|
|||
|
||||
let obj = js_sys::Object::new();
|
||||
|
||||
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str("_id"), &JsValue::from(comm.get_id()));
|
||||
let _ = js_sys::Reflect::set(
|
||||
&obj,
|
||||
&JsValue::from_str("_id"),
|
||||
&JsValue::from(comm.get_id()),
|
||||
);
|
||||
|
||||
let type_name = communication_type_name(comm.get_type().0).unwrap_or("Unknown");
|
||||
let _ = js_sys::Reflect::set(
|
||||
|
|
@ -196,12 +227,21 @@ pub fn parse_response_frame(frame: &[u8]) -> Result<String, JsValue> {
|
|||
}
|
||||
}
|
||||
|
||||
let stringified = js_sys::JSON::stringify(&obj)
|
||||
.map_err(|_| js_error("JSON stringify failed"))?;
|
||||
stringified.as_string()
|
||||
let stringified =
|
||||
js_sys::JSON::stringify(&obj).map_err(|_| js_error("JSON stringify failed"))?;
|
||||
stringified
|
||||
.as_string()
|
||||
.ok_or_else(|| js_error("JSON stringify result not a string"))
|
||||
}
|
||||
|
||||
/// 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)))?;
|
||||
Ok(comm.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod tests {
|
||||
|
|
@ -215,8 +255,14 @@ mod tests {
|
|||
|
||||
assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping
|
||||
assert_eq!(cv.get_sender(), 42);
|
||||
assert_eq!(cv.get_data(DataTypeId(4)), &DataValue::Str("test-ping".into()));
|
||||
assert_eq!(cv.get_data(DataTypeId(5)), &DataValue::UnsignedNumber(1234567890));
|
||||
assert_eq!(
|
||||
cv.get_data(DataTypeId(4)),
|
||||
&DataValue::Str("test-ping".into())
|
||||
);
|
||||
assert_eq!(
|
||||
cv.get_data(DataTypeId(5)),
|
||||
&DataValue::UnsignedNumber(1234567890)
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
|
|
@ -227,9 +273,15 @@ mod tests {
|
|||
|
||||
assert_eq!(cv.get_type(), CommunicationTypeId(19));
|
||||
assert_eq!(cv.get_sender(), 99);
|
||||
assert_eq!(cv.get_data(DataTypeId(4)), &DataValue::Str("with-data".into()));
|
||||
assert_eq!(
|
||||
cv.get_data(DataTypeId(4)),
|
||||
&DataValue::Str("with-data".into())
|
||||
);
|
||||
assert_eq!(cv.get_data(DataTypeId(5)), &DataValue::UnsignedNumber(555));
|
||||
assert_eq!(cv.get_data(DataTypeId(6)), &DataValue::Bytes(payload.to_vec()));
|
||||
assert_eq!(
|
||||
cv.get_data(DataTypeId(6)),
|
||||
&DataValue::Bytes(payload.to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
|
|
@ -241,12 +293,13 @@ mod tests {
|
|||
|
||||
#[wasm_bindgen_test]
|
||||
fn build_demo_message_roundtrip() {
|
||||
// A full keyring is required: the demo now KEM-encrypts to its own
|
||||
// public key, so the KEM keypair must be real.
|
||||
// The demo KEM-encrypts to the host's bundle, so a real host keypair is
|
||||
// required; the client keyring only needs its Ed25519 signing key.
|
||||
let keyring = Keyring::generate();
|
||||
let keyring_bytes = keyring.to_bytes();
|
||||
let host_bundle = Keyring::generate().public_key_bundle().as_bytes();
|
||||
|
||||
let result = build_demo_message(7, &keyring_bytes);
|
||||
let result = build_demo_message(7, &keyring_bytes, &host_bundle);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let bytes = result.unwrap();
|
||||
|
|
@ -254,12 +307,16 @@ mod tests {
|
|||
|
||||
assert_eq!(cv.get_type(), CommunicationTypeId(19)); // Ping
|
||||
assert_eq!(cv.get_sender(), 7);
|
||||
assert_eq!(cv.get_data(DataTypeId(4)), &DataValue::Str("MTP WASM Demo".into()));
|
||||
assert_eq!(
|
||||
cv.get_data(DataTypeId(4)),
|
||||
&DataValue::Str("MTP WASM Demo".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn build_demo_message_invalid_keyring() {
|
||||
let result = build_demo_message(1, b"not-a-valid-keyring");
|
||||
let host_bundle = Keyring::generate().public_key_bundle().as_bytes();
|
||||
let result = build_demo_message(1, b"not-a-valid-keyring", &host_bundle);
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.as_string().unwrap().contains("invalid keyring"));
|
||||
|
|
@ -278,11 +335,13 @@ mod tests {
|
|||
let result = parse_auth_response(&resp).expect("parse failed");
|
||||
|
||||
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
||||
.ok().and_then(|v| v.as_bool());
|
||||
.ok()
|
||||
.and_then(|v| v.as_bool());
|
||||
assert_eq!(connected, Some(true));
|
||||
|
||||
let id = js_sys::Reflect::get(&result, &"assignedId".into())
|
||||
.ok().and_then(|v| v.as_f64());
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64());
|
||||
assert_eq!(id, Some(42.0));
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +355,8 @@ mod tests {
|
|||
let result = parse_auth_response(&resp).expect("parse failed");
|
||||
|
||||
let connected = js_sys::Reflect::get(&result, &"connected".into())
|
||||
.ok().and_then(|v| v.as_bool());
|
||||
.ok()
|
||||
.and_then(|v| v.as_bool());
|
||||
assert_eq!(connected, Some(false));
|
||||
|
||||
// rejected should have no assignedId
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ use web_sys::{WebTransport, WebTransportHash, WebTransportOptions};
|
|||
|
||||
use crate::error::js_error;
|
||||
|
||||
const CLOSE_FRAME_LEN: u32 = u32::MAX;
|
||||
|
||||
/// Given a `SendStream` (old API with `.writable` or new API where stream IS a WritableStream),
|
||||
/// return the object to call `.getWriter()` on.
|
||||
fn resolve_stream_writable(send_stream: &JsValue) -> Result<JsValue, JsValue> {
|
||||
|
|
@ -199,10 +201,17 @@ impl WasmTransport {
|
|||
}
|
||||
|
||||
if buffer.len() >= 4 {
|
||||
let frame_len =
|
||||
u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
|
||||
if 4 + frame_len <= buffer.len() {
|
||||
return Ok(buffer[4..4 + frame_len].to_vec());
|
||||
let frame_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||
if frame_len == CLOSE_FRAME_LEN {
|
||||
return Err(js_error("connection closed before frame"));
|
||||
}
|
||||
|
||||
let frame_len = frame_len as usize;
|
||||
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||
return Err(js_error("invalid frame length"));
|
||||
};
|
||||
if frame_end <= buffer.len() {
|
||||
return Ok(buffer[4..frame_end].to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -230,13 +239,7 @@ impl WasmTransport {
|
|||
let result = match read_fn.call0(&reader_val) {
|
||||
Ok(p) => match JsFuture::from(p.unchecked_into::<js_sys::Promise>()).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
let _ = on_error.call1(
|
||||
&JsValue::NULL,
|
||||
&JsValue::from_str(&format!("read stream failed: {:?}", e)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(_) => break,
|
||||
},
|
||||
Err(_) => break,
|
||||
};
|
||||
|
|
@ -302,29 +305,43 @@ impl WasmTransport {
|
|||
|
||||
// Extract all complete frames from the buffer
|
||||
while buffer.len() >= 4 {
|
||||
let frame_len =
|
||||
u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
|
||||
if 4 + frame_len > buffer.len() {
|
||||
let frame_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||
if frame_len == CLOSE_FRAME_LEN {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_len = frame_len as usize;
|
||||
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||
return Err(js_error("invalid frame length"));
|
||||
};
|
||||
if frame_end > buffer.len() {
|
||||
break;
|
||||
}
|
||||
let frame = buffer[4..4 + frame_len].to_vec();
|
||||
let frame = buffer[4..frame_end].to_vec();
|
||||
let arr = js_sys::Uint8Array::from(&frame[..]);
|
||||
let _ = on_message.call1(&JsValue::NULL, &arr);
|
||||
buffer.drain(..4 + frame_len);
|
||||
buffer.drain(..frame_end);
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining complete frames after stream closes
|
||||
while buffer.len() >= 4 {
|
||||
let frame_len =
|
||||
u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]) as usize;
|
||||
if 4 + frame_len > buffer.len() {
|
||||
let frame_len = u32::from_be_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]);
|
||||
if frame_len == CLOSE_FRAME_LEN {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let frame_len = frame_len as usize;
|
||||
let Some(frame_end) = 4usize.checked_add(frame_len) else {
|
||||
return Err(js_error("invalid frame length"));
|
||||
};
|
||||
if frame_end > buffer.len() {
|
||||
break;
|
||||
}
|
||||
let frame = buffer[4..4 + frame_len].to_vec();
|
||||
let frame = buffer[4..frame_end].to_vec();
|
||||
let arr = js_sys::Uint8Array::from(&frame[..]);
|
||||
let _ = on_message.call1(&JsValue::NULL, &arr);
|
||||
buffer.drain(..4 + frame_len);
|
||||
buffer.drain(..frame_end);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Reference in a new issue