WASM
This commit is contained in:
parent
ade0c3cde4
commit
298253d6fa
31 changed files with 2899 additions and 276 deletions
|
|
@ -7,6 +7,7 @@ members = [
|
|||
"transport",
|
||||
"host",
|
||||
"client",
|
||||
"wasm",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -86,16 +86,3 @@ The type-map build script reads a YAML configuration to generate `CommunicationT
|
|||
| sha2 | No | Standard construction; widely reviewed |
|
||||
| zeroize | No | Simple; widely used |
|
||||
| mlkem-tls | No | mlkem-rs backend unaudited |
|
||||
|
||||
## Test Coverage
|
||||
|
||||
| Crate | Tests | Notes |
|
||||
| -------------- | ----- | ------------------------------------------ |
|
||||
| common | 7 | Error type Display, Clone, format |
|
||||
| type-map | 3 | Registry builtin, negotiate, latest |
|
||||
| codec | 32 | DataValue roundtrip, accessors, Display, Hash, base64; CommunicationValue frame encode/decode |
|
||||
| crypto | 20 | AEAD, signatures, KEM, KDF, hash, key types, multi-encrypt |
|
||||
| transport | 12 | ConnectionHandle state transitions; Policy defaults; SendMode |
|
||||
| host | 3 | Version extraction from messages |
|
||||
| client | 7 | ClientConfig; AuthState derives |
|
||||
| **Total** | **84** | |
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
thiserror = "2.0.18"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
wtransport = { version = "0.7.1", default-features = false, features = [
|
||||
"aws-lc-rs",
|
||||
"quinn",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use thiserror::Error;
|
||||
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq)]
|
||||
pub enum CodecError {
|
||||
#[error("Unknown version")]
|
||||
|
|
@ -40,43 +38,19 @@ mod tests {
|
|||
let b = a.clone();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_display() {
|
||||
assert_eq!(format!("{}", CommunicationError::UseAfterClosed), "Use after Closed");
|
||||
assert_eq!(format!("{}", CommunicationError::StreamClosed), "Stream Closed");
|
||||
assert_eq!(format!("{}", CommunicationError::StreamError), "Stream Error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_clone() {
|
||||
let a = CommunicationError::UseAfterClosed;
|
||||
let b = a.clone();
|
||||
assert_eq!(format!("{:?}", a), format!("{:?}", b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_authentication_failed() {
|
||||
let e = CommunicationError::AuthenticationFailed("bad key".into());
|
||||
assert!(format!("{}", e).contains("bad key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_other() {
|
||||
let e = CommunicationError::Other("custom error".into());
|
||||
assert!(format!("{}", e).contains("custom error"));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// CommunicationError
|
||||
//
|
||||
// On native targets the full variant set (including quinn / wtransport
|
||||
// wrappers) is available. On WASM only the transport-independent subset is
|
||||
// compiled.
|
||||
// ===========================================================================
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum CommunicationError {
|
||||
/* ====== NOTE on PartialEq ======
|
||||
* Manual PartialEq implementation below compares some inner
|
||||
* values (String, Vec<u8>) structurally and others only by
|
||||
* variant discriminant (types from quinn / wtransport that
|
||||
* don't themselves implement PartialEq). Keep this impl in
|
||||
* sync if variants are added or changed.
|
||||
*/
|
||||
#[error("Use after Closed")]
|
||||
UseAfterClosed,
|
||||
|
||||
|
|
@ -147,12 +121,58 @@ pub enum CommunicationError {
|
|||
Other(String),
|
||||
}
|
||||
|
||||
/* ===== Manual PartialEq =====
|
||||
*
|
||||
* Compare by variant, and structurally for String-holding variants.
|
||||
* Third-party error types (quinn, wtransport) are compared only by
|
||||
* discriminant since they don't implement PartialEq themselves.
|
||||
*/
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum CommunicationError {
|
||||
#[error("Use after Closed")]
|
||||
UseAfterClosed,
|
||||
|
||||
#[error("Connection closed by local shutdown")]
|
||||
ClosedLocally,
|
||||
|
||||
#[error("Connection closed by peer")]
|
||||
ClosedByPeer,
|
||||
|
||||
#[error("Connection terminated unexpectedly")]
|
||||
ConnectionLost,
|
||||
|
||||
#[error("ParseCommunicationValue error")]
|
||||
ParseCommunicationValue,
|
||||
|
||||
#[error("Parse Certificate error")]
|
||||
CertificateParseFailed,
|
||||
|
||||
#[error("Loading Certificate error")]
|
||||
CertificateLoadFailed,
|
||||
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Connecting error: {0}")]
|
||||
ConnectingError(String),
|
||||
|
||||
#[error("Message too large")]
|
||||
MessageTooLarge,
|
||||
|
||||
#[error("Stream Closed")]
|
||||
StreamClosed,
|
||||
|
||||
#[error("Stream Error")]
|
||||
StreamError,
|
||||
|
||||
#[error("Crypto Provider Install Error")]
|
||||
CryptoProviderInstallFailed,
|
||||
|
||||
#[error("Authentication failed: {0}")]
|
||||
AuthenticationFailed(String),
|
||||
|
||||
#[error("Other: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
// ---- manual PartialEq (quinn / wtransport types don't impl PartialEq) ----
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl PartialEq for CommunicationError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
|
|
@ -184,4 +204,71 @@ impl PartialEq for CommunicationError {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl PartialEq for CommunicationError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::UseAfterClosed, Self::UseAfterClosed) => true,
|
||||
(Self::ClosedLocally, Self::ClosedLocally) => true,
|
||||
(Self::ClosedByPeer, Self::ClosedByPeer) => true,
|
||||
(Self::ConnectionLost, Self::ConnectionLost) => true,
|
||||
(Self::ParseCommunicationValue, Self::ParseCommunicationValue) => true,
|
||||
(Self::CertificateParseFailed, Self::CertificateParseFailed) => true,
|
||||
(Self::CertificateLoadFailed, Self::CertificateLoadFailed) => true,
|
||||
(Self::ParseError(a), Self::ParseError(b)) => a == b,
|
||||
(Self::ConnectingError(a), Self::ConnectingError(b)) => a == b,
|
||||
(Self::MessageTooLarge, Self::MessageTooLarge) => true,
|
||||
(Self::StreamClosed, Self::StreamClosed) => true,
|
||||
(Self::StreamError, Self::StreamError) => true,
|
||||
(Self::CryptoProviderInstallFailed, Self::CryptoProviderInstallFailed) => true,
|
||||
(Self::AuthenticationFailed(a), Self::AuthenticationFailed(b)) => a == b,
|
||||
(Self::Other(a), Self::Other(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl Eq for CommunicationError {}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl Eq for CommunicationError {}
|
||||
|
||||
/* ================================ TESTS ================================ */
|
||||
#[cfg(test)]
|
||||
mod communication_error_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_display() {
|
||||
assert_eq!(format!("{}", CommunicationError::UseAfterClosed), "Use after Closed");
|
||||
assert_eq!(format!("{}", CommunicationError::StreamClosed), "Stream Closed");
|
||||
assert_eq!(format!("{}", CommunicationError::StreamError), "Stream Error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_clone() {
|
||||
let a = CommunicationError::UseAfterClosed;
|
||||
let b = a.clone();
|
||||
assert_eq!(format!("{:?}", a), format!("{:?}", b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_authentication_failed() {
|
||||
let e = CommunicationError::AuthenticationFailed("bad key".into());
|
||||
assert!(format!("{}", e).contains("bad key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_communication_error_other() {
|
||||
let e = CommunicationError::Other("custom error".into());
|
||||
assert!(format!("{}", e).contains("custom error"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn test_connecting_error_display() {
|
||||
let e = CommunicationError::ConnectingError("refused".into());
|
||||
assert!(format!("{}", e).contains("refused"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,3 +26,4 @@ default = ["chacha20poly1305", "ed25519-dalek", "hkdf", "sha2", "ml-dsa"]
|
|||
full = ["default", "aes-gcm"]
|
||||
pqc = ["mlkem-tls", "ml-dsa"]
|
||||
serde = ["dep:serde"]
|
||||
wasm = ["getrandom/wasm_js"]
|
||||
|
|
|
|||
1
example-usage/.gitignore
vendored
1
example-usage/.gitignore
vendored
|
|
@ -4,3 +4,4 @@ host_sig_pk.bin
|
|||
host_sig_pq_pk.bin
|
||||
host_enc_kem_pk.bin
|
||||
clients.json
|
||||
web-client/node_modules
|
||||
|
|
|
|||
52
example-usage/client/src/auth.rs
Normal file
52
example-usage/client/src/auth.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use std::fs;
|
||||
|
||||
use mtp::client::{ClientConfig, MTPClient, MTPConnection};
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner, PublicKeyBundle};
|
||||
|
||||
pub async fn connect_or_register(
|
||||
mut config: ClientConfig,
|
||||
host_public_key: PublicKeyBundle,
|
||||
client_key_path: &str,
|
||||
) -> Result<(MTPConnection, Keyring), Box<dyn std::error::Error>> {
|
||||
if let Ok(data) = fs::read_to_string(client_key_path) {
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
let client_id = json["client_id"].as_u64().expect("Invalid client_id");
|
||||
let keyring = Keyring::from_bytes(&hex::decode(
|
||||
json["keyring"].as_str().expect("Missing keyring"),
|
||||
)?)?;
|
||||
|
||||
println!("Loaded client keys (ID: {})", client_id);
|
||||
|
||||
config.client_id = client_id;
|
||||
let conn = MTPClient::auth_connect(config, &keyring, &host_public_key).await?;
|
||||
println!("Authenticated (version {})", conn.version);
|
||||
Ok((conn, keyring))
|
||||
} else {
|
||||
println!("No existing keys found: registering new client");
|
||||
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let keyring = Keyring::new(
|
||||
mtp::crypto::KemPublicKey::new(vec![]),
|
||||
mtp::crypto::KemPrivateKey::new(vec![]),
|
||||
sig_pq_pk,
|
||||
sig_pq_sk,
|
||||
sig_pk,
|
||||
sig_sk,
|
||||
);
|
||||
|
||||
let keyring_bytes = keyring.to_bytes();
|
||||
let conn = MTPClient::auth_register(config, &keyring, &host_public_key).await?;
|
||||
println!("Registered with ID: {}", conn.client_id);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"client_id": conn.client_id,
|
||||
"keyring": hex::encode(&keyring_bytes),
|
||||
});
|
||||
fs::write(client_key_path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Saved client keys -> {client_key_path}");
|
||||
|
||||
let keyring = Keyring::from_bytes(&keyring_bytes)?;
|
||||
Ok((conn, keyring))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +1,22 @@
|
|||
mod auth;
|
||||
mod messages;
|
||||
|
||||
use std::fs;
|
||||
|
||||
use mtp::client::{ClientConfig, MTPClient, MTPConnection};
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use mtp::crypto::{
|
||||
Ed25519Signer, KemPublicKey, Keyring, MlDsaSigner, PublicKeyBundle, SignaturePqPublicKey,
|
||||
SignaturePublicKey,
|
||||
};
|
||||
|
||||
/*
|
||||
* Either load a saved keyring from disk (login) or generate a fresh
|
||||
* keypair and register with the server. Returns the established
|
||||
* connection and the local keyring.
|
||||
*/
|
||||
async fn connect_or_register(
|
||||
mut config: ClientConfig,
|
||||
host_public_key: PublicKeyBundle,
|
||||
client_key_path: &str,
|
||||
) -> Result<(MTPConnection, Keyring), Box<dyn std::error::Error>> {
|
||||
if let Ok(data) = fs::read_to_string(client_key_path) {
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
let client_id = json["client_id"].as_u64().expect("Invalid client_id");
|
||||
let keyring = Keyring::from_bytes(&hex::decode(
|
||||
json["keyring"].as_str().expect("Missing keyring"),
|
||||
)?)?;
|
||||
|
||||
println!("Loaded client keys (ID: {})", client_id);
|
||||
|
||||
config.client_id = client_id;
|
||||
let conn = MTPClient::auth_connect(config, &keyring, &host_public_key).await?;
|
||||
println!("Authenticated (version {})", conn.version);
|
||||
Ok((conn, keyring))
|
||||
} else {
|
||||
println!("No existing keys found: registering new client");
|
||||
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let keyring = Keyring::new(
|
||||
mtp::crypto::KemPublicKey::new(vec![]),
|
||||
mtp::crypto::KemPrivateKey::new(vec![]),
|
||||
sig_pq_pk,
|
||||
sig_pq_sk,
|
||||
sig_pk,
|
||||
sig_sk,
|
||||
);
|
||||
|
||||
let keyring_bytes = keyring.to_bytes();
|
||||
let conn = MTPClient::auth_register(config, &keyring, &host_public_key).await?;
|
||||
println!("Registered with ID: {}", conn.client_id);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"client_id": conn.client_id,
|
||||
"keyring": hex::encode(&keyring_bytes),
|
||||
});
|
||||
fs::write(client_key_path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Saved client keys -> {client_key_path}");
|
||||
|
||||
let keyring = Keyring::from_bytes(&keyring_bytes)?;
|
||||
Ok((conn, keyring))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Send a Ping greeting with description, timestamp, and data fields,
|
||||
* then print the server's Pong response.
|
||||
*/
|
||||
async fn send_ping_and_receive(conn: &MTPConnection) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let greeting = CommunicationValue::new(CommunicationType::Ping)
|
||||
.add_typed_default(DataType::Description, DataValue::Str("Hello MTP!".into()))
|
||||
.add_typed_default(
|
||||
DataType::Timestamp,
|
||||
DataValue::UnsignedNumber(timestamp as u128),
|
||||
)
|
||||
.add_typed_default(DataType::Data, DataValue::UnsignedNumber(42))
|
||||
.with_sender(conn.client_id);
|
||||
|
||||
println!("Sending: {greeting}");
|
||||
conn.sender.send(&greeting).await?;
|
||||
|
||||
match conn.receiver.receive().await {
|
||||
Ok(msg) => println!("Received: {msg}"),
|
||||
Err(e) => eprintln!("Receive error: {e}"),
|
||||
}
|
||||
|
||||
conn.sender.close();
|
||||
Ok(())
|
||||
}
|
||||
use mtp::client::ClientConfig;
|
||||
use mtp::crypto::{KemPublicKey, PublicKeyBundle, SignaturePqPublicKey, SignaturePublicKey};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cert_pem = fs::read("server.pem").expect("Missing server.pem: run server first");
|
||||
let host_public_key = PublicKeyBundle::new(
|
||||
KemPublicKey::new(
|
||||
fs::read("host_enc_kem_pk.bin").expect("Missing host_enc_kem_pk.bin: run server first"),
|
||||
fs::read("host_enc_kem_pk.bin")
|
||||
.expect("Missing host_enc_kem_pk.bin: run server first"),
|
||||
),
|
||||
SignaturePqPublicKey::new(
|
||||
fs::read("host_sig_pq_pk.bin").expect("Missing host_sig_pq_pk.bin: run server first"),
|
||||
fs::read("host_sig_pq_pk.bin")
|
||||
.expect("Missing host_sig_pq_pk.bin: run server first"),
|
||||
),
|
||||
SignaturePublicKey::new(
|
||||
fs::read("host_sig_pk.bin").expect("Missing host_sig_pk.bin: run server first"),
|
||||
|
|
@ -114,8 +31,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
client_id: 0,
|
||||
};
|
||||
|
||||
let (conn, _keyring) = connect_or_register(config, host_public_key, "client_keys.json").await?;
|
||||
send_ping_and_receive(&conn).await?;
|
||||
let (conn, keyring) =
|
||||
auth::connect_or_register(config, host_public_key, "client_keys.json").await?;
|
||||
messages::send_and_receive(&conn, &keyring).await?;
|
||||
|
||||
println!("\nDone");
|
||||
Ok(())
|
||||
|
|
|
|||
81
example-usage/client/src/messages.rs
Normal file
81
example-usage/client/src/messages.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use mtp::client::MTPConnection;
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue};
|
||||
use mtp::crypto::{ChaCha20Poly1305, Ed25519Signer, Keyring, SigAlgorithm};
|
||||
|
||||
fn derive_demo_key() -> [u8; 32] {
|
||||
mtp::crypto::derive_encryption_key(
|
||||
b"MTP-demo-shared-secret",
|
||||
b"MTP-demo-salt",
|
||||
b"encrypted-container-demo",
|
||||
)
|
||||
.expect("key derivation must succeed")
|
||||
}
|
||||
|
||||
pub fn build_demo_message(client_id: u64, keyring: &Keyring) -> CommunicationValue {
|
||||
let cipher = ChaCha20Poly1305::new(derive_demo_key());
|
||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.expect("Ed25519 signer from keyring");
|
||||
|
||||
let inner_enc = DataValue::Container(vec![
|
||||
(DataTypeId(1), DataValue::Str("secret inner data".into())),
|
||||
(DataTypeId(2), DataValue::UnsignedNumber(42)),
|
||||
]);
|
||||
let mut dv_enc = inner_enc;
|
||||
dv_enc.encrypt_container(&cipher, b"demo-aad");
|
||||
|
||||
let inner_sig = DataValue::Container(vec![
|
||||
(DataTypeId(1), DataValue::Str("signed by client".into())),
|
||||
(DataTypeId(2), DataValue::UnsignedNumber(99)),
|
||||
]);
|
||||
let mut dv_sig = inner_sig;
|
||||
dv_sig.sign_container(SigAlgorithm::ED25519, &signer);
|
||||
|
||||
let inner_sec = DataValue::Container(vec![
|
||||
(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, &cipher, b"demo-aad");
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
CommunicationValue::new(CommunicationType::Ping)
|
||||
.add_typed_default(DataType::Description, DataValue::Str("MTP Data Type Demo".into()))
|
||||
.add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(timestamp as u128))
|
||||
.add_typed_default(DataType::Data, DataValue::Str("Hello, MTP!".into()))
|
||||
.add_typed_default(DataType::Flags, DataValue::BoolTrue)
|
||||
.add_typed_default(DataType::Value, DataValue::Float(2, 12345))
|
||||
.add_typed_default(DataType::BinaryData, DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF, 0x42]))
|
||||
.add_typed_default(
|
||||
DataType::Items,
|
||||
DataValue::Array(vec![
|
||||
DataValue::Str("alpha".into()),
|
||||
DataValue::Str("beta".into()),
|
||||
DataValue::Str("gamma".into()),
|
||||
]),
|
||||
)
|
||||
.add_typed_default(DataType::EncryptedPayload, dv_enc)
|
||||
.add_typed_default(DataType::SignedPayload, dv_sig)
|
||||
.add_typed_default(DataType::SecurePayload, dv_sec)
|
||||
.with_sender(client_id)
|
||||
}
|
||||
|
||||
pub async fn send_and_receive(
|
||||
conn: &MTPConnection,
|
||||
keyring: &Keyring,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let msg = build_demo_message(conn.client_id, keyring);
|
||||
println!("Sending: {msg}");
|
||||
conn.sender.send(&msg).await?;
|
||||
|
||||
match conn.receiver.receive().await {
|
||||
Ok(resp) => println!("Received: {resp}"),
|
||||
Err(e) => eprintln!("Receive error: {e}"),
|
||||
}
|
||||
|
||||
conn.sender.close();
|
||||
Ok(())
|
||||
}
|
||||
21
example-usage/server/src/clients.rs
Normal file
21
example-usage/server/src/clients.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use mtp::crypto::PublicKeyBundle;
|
||||
|
||||
pub fn load_client_db(
|
||||
path: &str,
|
||||
) -> Result<(Arc<Mutex<HashMap<u64, PublicKeyBundle>>>, Arc<Mutex<u64>>), Box<dyn std::error::Error>>
|
||||
{
|
||||
let clients: Arc<Mutex<HashMap<u64, PublicKeyBundle>>> =
|
||||
Arc::new(Mutex::new(if let Ok(data) = fs::read_to_string(path) {
|
||||
serde_json::from_str(&data).unwrap_or_default()
|
||||
} else {
|
||||
HashMap::new()
|
||||
}));
|
||||
let next_id = Arc::new(Mutex::new(
|
||||
clients.lock().unwrap().keys().max().unwrap_or(&999) + 1,
|
||||
));
|
||||
Ok((clients, next_id))
|
||||
}
|
||||
139
example-usage/server/src/handlers.rs
Normal file
139
example-usage/server/src/handlers.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||
use mtp::crypto::{ChaCha20Poly1305, CryptoError, SignatureScheme, SignaturePublicKey, verify_ed25519};
|
||||
|
||||
struct Ed25519Verifier(SignaturePublicKey);
|
||||
|
||||
impl SignatureScheme for Ed25519Verifier {
|
||||
fn sign(&self, _msg: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
Err(CryptoError::SigningFailed)
|
||||
}
|
||||
fn verify(&self, msg: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
|
||||
verify_ed25519(&self.0, msg, signature)
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_demo_key() -> [u8; 32] {
|
||||
mtp::crypto::derive_encryption_key(
|
||||
b"MTP-demo-shared-secret",
|
||||
b"MTP-demo-salt",
|
||||
b"encrypted-container-demo",
|
||||
)
|
||||
.expect("key derivation must succeed")
|
||||
}
|
||||
|
||||
pub fn process_and_respond(
|
||||
msg: &CommunicationValue,
|
||||
tm: &TypeMap,
|
||||
client_pk: Option<&mtp::crypto::PublicKeyBundle>,
|
||||
) -> CommunicationValue {
|
||||
let desc_id = DataTypeId(tm.data_id_enum(DataType::Description).unwrap());
|
||||
let ts_id = DataTypeId(tm.data_id_enum(DataType::Timestamp).unwrap());
|
||||
let data_id = DataTypeId(tm.data_id_enum(DataType::Data).unwrap());
|
||||
let flags_id = DataTypeId(tm.data_id_enum(DataType::Flags).unwrap());
|
||||
let value_id = DataTypeId(tm.data_id_enum(DataType::Value).unwrap());
|
||||
let bin_id = DataTypeId(tm.data_id_enum(DataType::BinaryData).unwrap());
|
||||
let items_id = DataTypeId(tm.data_id_enum(DataType::Items).unwrap());
|
||||
let enc_id = DataTypeId(tm.data_id_enum(DataType::EncryptedPayload).unwrap());
|
||||
let sig_id = DataTypeId(tm.data_id_enum(DataType::SignedPayload).unwrap());
|
||||
let secure_id = DataTypeId(tm.data_id_enum(DataType::SecurePayload).unwrap());
|
||||
|
||||
let description = msg.get_data(desc_id);
|
||||
let timestamp = msg.get_data(ts_id);
|
||||
let data = msg.get_data(data_id);
|
||||
let flags = msg.get_data(flags_id);
|
||||
let value = msg.get_data(value_id);
|
||||
let binary = msg.get_data(bin_id);
|
||||
let items = msg.get_data(items_id);
|
||||
|
||||
println!(
|
||||
" Description: {}",
|
||||
description.as_str().unwrap_or("(missing)")
|
||||
);
|
||||
println!(" Timestamp: {:?}", timestamp.as_unsigned_number());
|
||||
println!(" Data: {}", data.as_str().unwrap_or("(missing)"));
|
||||
println!(" Flags: {:?}", flags.as_bool());
|
||||
println!(" Value: {:?}", value.as_float());
|
||||
println!(" Binary: {:?}", binary.as_bytes());
|
||||
println!(" Items: {:?}", items.as_array());
|
||||
|
||||
let cipher = ChaCha20Poly1305::new(derive_demo_key());
|
||||
|
||||
let mut enc_status = String::from("EncryptedPayload: not present");
|
||||
let mut sig_status = String::from("SignedPayload: not present");
|
||||
let mut secure_status = String::from("SecurePayload: not present");
|
||||
|
||||
let enc = msg.get_data(enc_id);
|
||||
if matches!(enc, DataValue::EncryptedContainer(_)) {
|
||||
let mut dv = enc.clone();
|
||||
if dv.decrypt_into_container(&cipher, b"demo-aad").is_some() {
|
||||
if let Some(entries) = dv.as_container() {
|
||||
println!(" Decrypted EncryptedPayload: {:?}", entries);
|
||||
enc_status = format!("EncryptedPayload decrypted OK ({} entries)", entries.len());
|
||||
}
|
||||
} else {
|
||||
enc_status = String::from("EncryptedPayload: decryption FAILED");
|
||||
}
|
||||
}
|
||||
|
||||
let sig = msg.get_data(sig_id);
|
||||
if matches!(sig, DataValue::SignedContainer(_)) {
|
||||
if let Some(pk_bundle) = client_pk {
|
||||
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
||||
let mut dv = sig.clone();
|
||||
if dv.verify_into_container(&verifier).is_some() {
|
||||
if let Some(entries) = dv.as_container() {
|
||||
println!(" Verified SignedPayload: {:?}", entries);
|
||||
sig_status =
|
||||
format!("SignedPayload verified OK ({} entries)", entries.len());
|
||||
}
|
||||
} else {
|
||||
sig_status = String::from("SignedPayload: verification FAILED");
|
||||
}
|
||||
} else {
|
||||
sig_status = String::from("SignedPayload: no client public key available");
|
||||
}
|
||||
}
|
||||
|
||||
let secure = msg.get_data(secure_id);
|
||||
if matches!(secure, DataValue::SignedEncryptedContainer(_)) {
|
||||
if let Some(pk_bundle) = client_pk {
|
||||
let verifier = Ed25519Verifier(pk_bundle.sig_cl_public_key.clone());
|
||||
let mut dv = secure.clone();
|
||||
if dv.decrypt_signed_encrypted_container(&cipher, b"demo-aad").is_some()
|
||||
&& dv.verify_into_container(&verifier).is_some()
|
||||
{
|
||||
if let Some(entries) = dv.as_container() {
|
||||
println!(" Verified SecurePayload: {:?}", entries);
|
||||
secure_status = format!(
|
||||
"SecurePayload decrypted+verified OK ({} entries)",
|
||||
entries.len()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
secure_status = String::from("SecurePayload: decryption/verification FAILED");
|
||||
}
|
||||
} else {
|
||||
secure_status = String::from("SecurePayload: no client public key available");
|
||||
}
|
||||
}
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||
.add_data(desc_id, description.clone())
|
||||
.add_data(ts_id, DataValue::UnsignedNumber(now as u128))
|
||||
.add_data(
|
||||
data_id,
|
||||
DataValue::Str(format!(
|
||||
"{}. {}. {}.",
|
||||
enc_status, sig_status, secure_status
|
||||
)),
|
||||
)
|
||||
.add_data(flags_id, flags.clone())
|
||||
.add_data(value_id, value.clone())
|
||||
.add_data(bin_id, binary.clone())
|
||||
.add_data(items_id, items.clone())
|
||||
}
|
||||
40
example-usage/server/src/keys.rs
Normal file
40
example-usage/server/src/keys.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use std::fs;
|
||||
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner};
|
||||
use mtp::crypto::kem::HybridKem;
|
||||
|
||||
pub fn load_or_generate_host_keys(path: &str) -> Result<(u64, Keyring), Box<dyn std::error::Error>> {
|
||||
if let Ok(data) = fs::read_to_string(path) {
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
let hid = json["host_id"].as_u64().unwrap_or(1);
|
||||
let keyring = Keyring::from_bytes(&hex::decode(json["keyring"].as_str().unwrap())?)?;
|
||||
println!("Loaded host keys (ID: {})", hid);
|
||||
return Ok((hid, keyring));
|
||||
}
|
||||
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let (kem_sk, kem_pk) = HybridKem::generate_keypair();
|
||||
let keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"host_id": 1,
|
||||
"keyring": hex::encode(keyring.to_bytes()),
|
||||
});
|
||||
fs::write(path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Generated host keys -> {path}");
|
||||
Ok((1u64, keyring))
|
||||
}
|
||||
|
||||
pub fn export_host_public_keys(host_keyring: &Keyring) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fs::write(
|
||||
"host_enc_kem_pk.bin",
|
||||
host_keyring.kem_public_key.as_bytes(),
|
||||
)?;
|
||||
fs::write("host_sig_pk.bin", host_keyring.sig_cl_public_key.as_bytes())?;
|
||||
fs::write(
|
||||
"host_sig_pq_pk.bin",
|
||||
host_keyring.sig_pq_public_key.as_bytes(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,149 +1,34 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::sync::{Arc, Mutex};
|
||||
mod clients;
|
||||
mod handlers;
|
||||
mod keys;
|
||||
mod tls;
|
||||
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, TypeMap};
|
||||
use mtp::crypto::{Ed25519Signer, Keyring, MlDsaSigner, PublicKeyBundle, kem::HybridKem};
|
||||
use mtp::host::{HostConfig, MTPHost};
|
||||
|
||||
/*
|
||||
* Load an existing TLS certificate and key pair from disk. If neither
|
||||
* file exists, generate a self-signed certificate so the server is
|
||||
* immediately usable without external tooling.
|
||||
*/
|
||||
fn load_or_generate_tls(
|
||||
cert_path: &str,
|
||||
key_path: &str,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error>> {
|
||||
if let (Ok(c), Ok(k)) = (fs::read(cert_path), fs::read(key_path)) {
|
||||
println!("Using existing TLS cert from {cert_path}");
|
||||
return Ok((c, k));
|
||||
}
|
||||
|
||||
println!("Generating self-signed TLS certificate ...");
|
||||
let key_pair = rcgen::KeyPair::generate()?;
|
||||
let params = rcgen::CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()])?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_str = cert.pem();
|
||||
let key_str = key_pair.serialize_pem();
|
||||
|
||||
fs::write(cert_path, cert_str.as_bytes())?;
|
||||
fs::write(key_path, key_str.as_bytes())?;
|
||||
println!("Wrote {cert_path} and {key_path}");
|
||||
|
||||
Ok((cert_str.into_bytes(), key_str.into_bytes()))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load the host's Ed25519 identity from a JSON file, or generate a
|
||||
* fresh one and persist it. Clients need the corresponding public key
|
||||
* (exported separately as host_sig_pk.bin) to authenticate the host
|
||||
* during the handshake.
|
||||
*/
|
||||
fn load_or_generate_host_keys(path: &str) -> Result<(u64, Keyring), Box<dyn std::error::Error>> {
|
||||
if let Ok(data) = fs::read_to_string(path) {
|
||||
let json: serde_json::Value = serde_json::from_str(&data)?;
|
||||
let hid = json["host_id"].as_u64().unwrap_or(1);
|
||||
let keyring = Keyring::from_bytes(&hex::decode(json["keyring"].as_str().unwrap())?)?;
|
||||
println!("Loaded host keys (ID: {})", hid);
|
||||
return Ok((hid, keyring));
|
||||
}
|
||||
|
||||
let (_ed_signer, sig_sk, sig_pk) = Ed25519Signer::generate();
|
||||
let (_pq_signer, sig_pq_sk, sig_pq_pk) = MlDsaSigner::generate();
|
||||
let (kem_sk, kem_pk) = HybridKem::generate_keypair();
|
||||
let keyring = Keyring::new(kem_pk, kem_sk, sig_pq_pk, sig_pq_sk, sig_pk, sig_sk);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"host_id": 1,
|
||||
"keyring": hex::encode(keyring.to_bytes()),
|
||||
});
|
||||
fs::write(path, serde_json::to_string_pretty(&json)?)?;
|
||||
println!("Generated host keys -> {path}");
|
||||
Ok((1u64, keyring))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load the client database from disk. Each entry maps a numeric
|
||||
* client ID to its PublicKeyBundle. next_id starts one past the
|
||||
* highest known ID (or 1000 if the DB is empty).
|
||||
*/
|
||||
fn load_client_db(
|
||||
path: &str,
|
||||
) -> Result<(Arc<Mutex<HashMap<u64, PublicKeyBundle>>>, Arc<Mutex<u64>>), Box<dyn std::error::Error>>
|
||||
{
|
||||
let clients: Arc<Mutex<HashMap<u64, PublicKeyBundle>>> =
|
||||
Arc::new(Mutex::new(if let Ok(data) = fs::read_to_string(path) {
|
||||
serde_json::from_str(&data).unwrap_or_default()
|
||||
} else {
|
||||
HashMap::new()
|
||||
}));
|
||||
let next_id = Arc::new(Mutex::new(
|
||||
clients.lock().unwrap().keys().max().unwrap_or(&999) + 1,
|
||||
));
|
||||
Ok((clients, next_id))
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a Pong response carrying a description, a Unix timestamp, and
|
||||
* a custom payload according to the negotiated type map.
|
||||
*/
|
||||
fn build_pong_response(tm: &TypeMap) -> CommunicationValue {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||
.add_data(
|
||||
DataTypeId(tm.data_id_enum(DataType::Description).unwrap()),
|
||||
DataValue::Str("Hello from server!".into()),
|
||||
)
|
||||
.add_data(
|
||||
DataTypeId(tm.data_id_enum(DataType::Timestamp).unwrap()),
|
||||
DataValue::UnsignedNumber(timestamp as u128),
|
||||
)
|
||||
.add_data(
|
||||
DataTypeId(tm.data_id_enum(DataType::Data).unwrap()),
|
||||
DataValue::Str("custom payload".into()),
|
||||
)
|
||||
}
|
||||
use mtp::type_map::TypeMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (cert_pem, key_pem) = load_or_generate_tls("server.pem", "server.key")?;
|
||||
let (host_id, host_keyring) = load_or_generate_host_keys("host_keys.json")?;
|
||||
let (cert_pem, key_pem) = tls::load_or_generate_tls("server.pem", "server.key")?;
|
||||
let (host_id, host_keyring) = keys::load_or_generate_host_keys("host_keys.json")?;
|
||||
keys::export_host_public_keys(&host_keyring)?;
|
||||
|
||||
// Export the host's public keys so clients can verify it
|
||||
fs::write(
|
||||
"host_enc_kem_pk.bin",
|
||||
host_keyring.kem_public_key.as_bytes(),
|
||||
)?;
|
||||
fs::write("host_sig_pk.bin", host_keyring.sig_cl_public_key.as_bytes())?;
|
||||
fs::write(
|
||||
"host_sig_pq_pk.bin",
|
||||
host_keyring.sig_pq_public_key.as_bytes(),
|
||||
)?;
|
||||
let (clients, next_id) = clients::load_client_db("clients.json")?;
|
||||
|
||||
let (clients, next_id) = load_client_db("clients.json")?;
|
||||
|
||||
// Clone the Arc so each closure owns its own reference
|
||||
let clients_for_get = clients.clone();
|
||||
let get_existing_user = Box::new(move |id: u64| -> Option<PublicKeyBundle> {
|
||||
let get_existing_user = Box::new(move |id: u64| -> Option<mtp::crypto::PublicKeyBundle> {
|
||||
clients_for_get.lock().unwrap().get(&id).cloned()
|
||||
});
|
||||
|
||||
let clients_for_register = clients.clone();
|
||||
let next_id_for_register = next_id.clone();
|
||||
let clients_path = "clients.json".to_string();
|
||||
let complete_register = Box::new(move |bundle: PublicKeyBundle| -> u64 {
|
||||
let complete_register = Box::new(move |bundle: mtp::crypto::PublicKeyBundle| -> u64 {
|
||||
let mut db = clients_for_register.lock().unwrap();
|
||||
let mut nid = next_id_for_register.lock().unwrap();
|
||||
let id = *nid;
|
||||
*nid += 1;
|
||||
db.insert(id, bundle);
|
||||
fs::write(&clients_path, serde_json::to_string_pretty(&*db).unwrap()).ok();
|
||||
std::fs::write(&clients_path, serde_json::to_string_pretty(&*db).unwrap()).ok();
|
||||
println!("Registered new client with ID: {}", id);
|
||||
id
|
||||
});
|
||||
|
|
@ -177,7 +62,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
match conn.receiver.receive().await {
|
||||
Ok(msg) => {
|
||||
println!("Received: {msg}");
|
||||
let response = build_pong_response(tm);
|
||||
let response =
|
||||
handlers::process_and_respond(&msg, tm, conn.client_public_key.as_ref());
|
||||
println!("Sending: {response}");
|
||||
conn.sender.send(&response).await?;
|
||||
}
|
||||
|
|
|
|||
25
example-usage/server/src/tls.rs
Normal file
25
example-usage/server/src/tls.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use std::fs;
|
||||
|
||||
pub fn load_or_generate_tls(
|
||||
cert_path: &str,
|
||||
key_path: &str,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), Box<dyn std::error::Error>> {
|
||||
if let (Ok(c), Ok(k)) = (fs::read(cert_path), fs::read(key_path)) {
|
||||
println!("Using existing TLS cert from {cert_path}");
|
||||
return Ok((c, k));
|
||||
}
|
||||
|
||||
println!("Generating self-signed TLS certificate ...");
|
||||
let key_pair = rcgen::KeyPair::generate()?;
|
||||
let params = rcgen::CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()])?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_str = cert26.pem();
|
||||
let key_str = key_pair.serialize_pem();
|
||||
|
||||
fs::write(cert_path, cert_str.as_bytes())?;
|
||||
fs::write(key_path, key_str.as_bytes())?;
|
||||
println!("Wrote {cert_path} and {key_path}");
|
||||
|
||||
Ok((cert_str.into_bytes(), key_str.into_bytes()))
|
||||
}
|
||||
|
|
@ -8,7 +8,21 @@ type_maps:
|
|||
CommunicationTypes:
|
||||
DataTypes:
|
||||
Data: 32
|
||||
Flags: 33
|
||||
Value: 34
|
||||
BinaryData: 35
|
||||
Items: 36
|
||||
EncryptedPayload: 37
|
||||
SignedPayload: 38
|
||||
SecurePayload: 39
|
||||
"2.0":
|
||||
CommunicationTypes:
|
||||
DataTypes:
|
||||
Data: 34
|
||||
Flags: 33
|
||||
Value: 35
|
||||
BinaryData: 36
|
||||
Items: 37
|
||||
EncryptedPayload: 38
|
||||
SignedPayload: 39
|
||||
SecurePayload: 40
|
||||
|
|
|
|||
20
example-usage/web-client/index.html
Normal file
20
example-usage/web-client/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MTP Web Client</title>
|
||||
<style>
|
||||
body { font-family: monospace; background: #111; color: #0f0; padding: 2rem; }
|
||||
#status { white-space: pre-wrap; }
|
||||
.state { color: #ff0; }
|
||||
.received { color: #0ff; }
|
||||
.error { color: #f00; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MTP WebTransport Client</h1>
|
||||
<div id="status">Initializing...</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1041
example-usage/web-client/package-lock.json
generated
Normal file
1041
example-usage/web-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
15
example-usage/web-client/package.json
Normal file
15
example-usage/web-client/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "mtp-web-client",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0",
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
110
example-usage/web-client/src/main.ts
Normal file
110
example-usage/web-client/src/main.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import init, {
|
||||
WasmClient,
|
||||
ConnectionConfig,
|
||||
ConnectionState,
|
||||
WasmEd25519Signer,
|
||||
ed25519_generate,
|
||||
keyring_from_ed25519,
|
||||
build_demo_message,
|
||||
} from 'mtp-wasm';
|
||||
|
||||
const STATUS = document.getElementById('status')!;
|
||||
const STORAGE_KEY = 'mtp-web-client-keys';
|
||||
|
||||
function log(msg: string, cls = '') {
|
||||
const line = document.createElement('div');
|
||||
line.textContent = msg;
|
||||
if (cls) line.className = cls;
|
||||
STATUS.appendChild(line);
|
||||
}
|
||||
|
||||
function saveKeys(clientId: bigint, keyringBytes: Uint8Array) {
|
||||
const data = {
|
||||
clientId: clientId.toString(),
|
||||
keyring: Array.from(keyringBytes),
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
function loadKeys(): { clientId: bigint; keyringBytes: Uint8Array } | null {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const data = JSON.parse(raw);
|
||||
return {
|
||||
clientId: BigInt(data.clientId),
|
||||
keyringBytes: new Uint8Array(data.keyring),
|
||||
};
|
||||
}
|
||||
|
||||
async function initWasm() {
|
||||
log('Loading WASM module...');
|
||||
await init();
|
||||
log(`WASM loaded. WebTransport supported: ${WasmClient.is_supported()}`);
|
||||
}
|
||||
|
||||
function createClient(): WasmClient {
|
||||
return new WasmClient(
|
||||
(state: number) => log(`[state] ${ConnectionState[state] ?? state}`, 'state'),
|
||||
(data: Uint8Array) => {
|
||||
const decoder = new TextDecoder();
|
||||
log(`[message] ${data.length} bytes: ${decoder.decode(data)}`, 'received');
|
||||
},
|
||||
(err: any) => log(`[error] ${err}`, 'error'),
|
||||
);
|
||||
}
|
||||
|
||||
function generateKeyringBytes(): Uint8Array {
|
||||
const gen = ed25519_generate();
|
||||
const sk = gen.secretKey as Uint8Array;
|
||||
const pk = gen.publicKey as Uint8Array;
|
||||
gen.signer.free();
|
||||
return keyring_from_ed25519(sk, pk);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await initWasm();
|
||||
|
||||
if (!WasmClient.is_supported()) {
|
||||
log('WebTransport is not supported in this browser.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const serverUrl = 'https://127.0.0.1:8080';
|
||||
const saved = loadKeys();
|
||||
|
||||
const client = createClient();
|
||||
const config = new ConnectionConfig(serverUrl);
|
||||
|
||||
let clientId: bigint;
|
||||
let keyringBytes: Uint8Array;
|
||||
|
||||
if (saved) {
|
||||
log(`Found saved client keys (ID: ${saved.clientId})`);
|
||||
const hostPk = new Uint8Array(0);
|
||||
clientId = await client.auth_connect(config, hostPk, saved.keyringBytes, saved.clientId);
|
||||
log(`Authenticated as client ${clientId}`);
|
||||
keyringBytes = saved.keyringBytes;
|
||||
} else {
|
||||
log('No saved keys — registering new client...');
|
||||
const hostPk = new Uint8Array(0);
|
||||
keyringBytes = generateKeyringBytes();
|
||||
clientId = await client.auth_register(config, hostPk, keyringBytes);
|
||||
log(`Registered with ID: ${clientId}`);
|
||||
saveKeys(clientId, keyringBytes);
|
||||
log('Saved client keys to localStorage');
|
||||
}
|
||||
|
||||
config.free();
|
||||
|
||||
log('\nSending demo message...');
|
||||
const frame = build_demo_message(clientId, keyringBytes);
|
||||
await client.send(frame);
|
||||
log(`Sent ${frame.length} bytes`);
|
||||
|
||||
log('\nClient running. Waiting for incoming messages...');
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
log(`Fatal error: ${e}`, 'error');
|
||||
console.error(e);
|
||||
});
|
||||
26
example-usage/web-client/src/messages.ts
Normal file
26
example-usage/web-client/src/messages.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { build_demo_message, build_ping_frame, parse_auth_response } from 'mtp-wasm';
|
||||
|
||||
export function buildAuthResponse(
|
||||
response: Uint8Array,
|
||||
): {
|
||||
connected: boolean;
|
||||
clientNonce: Uint8Array;
|
||||
assignedId: bigint;
|
||||
timestamp: bigint;
|
||||
signature: Uint8Array;
|
||||
} {
|
||||
return parse_auth_response(response);
|
||||
}
|
||||
|
||||
export function buildDemoMessage(clientId: bigint, keyringBytes: Uint8Array): Uint8Array {
|
||||
return build_demo_message(clientId, keyringBytes);
|
||||
}
|
||||
|
||||
export function buildPingFrame(
|
||||
clientId: bigint,
|
||||
description: string,
|
||||
timestamp: bigint,
|
||||
data?: Uint8Array,
|
||||
): Uint8Array {
|
||||
return build_ping_frame(clientId, description, timestamp, data ?? new Uint8Array());
|
||||
}
|
||||
18
example-usage/web-client/tsconfig.json
Normal file
18
example-usage/web-client/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"mtp-wasm": ["../../wasm/pkg"]
|
||||
},
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"include": ["src", "../../wasm/pkg"]
|
||||
}
|
||||
10
example-usage/web-client/vite.config.ts
Normal file
10
example-usage/web-client/vite.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'mtp-wasm': path.resolve(__dirname, '../../wasm/pkg'),
|
||||
},
|
||||
},
|
||||
});
|
||||
68
flake.nix
Normal file
68
flake.nix
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
description = "MTP - Methanium Transport Protocol";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [
|
||||
"rust-src"
|
||||
"clippy"
|
||||
"rustfmt"
|
||||
];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
name = "mtp-dev";
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
rustToolchain
|
||||
wasm-pack
|
||||
pkg-config
|
||||
openssl
|
||||
];
|
||||
|
||||
MTP_TYPE_MAPS = "${toString ./example-usage/type-maps.yaml}";
|
||||
|
||||
shellHook = ''
|
||||
echo "MTP dev shell"
|
||||
echo " rustc : $(rustc --version)"
|
||||
echo " cargo : $(cargo --version)"
|
||||
echo " wasm-pack : $(wasm-pack --version 2>/dev/null || echo 'not found')"
|
||||
echo " targets: $(rustc --print target-list | grep wasm32 | tr '\n' ' ')"
|
||||
echo " MTP_TYPE_MAPS = $MTP_TYPE_MAPS"
|
||||
'';
|
||||
};
|
||||
|
||||
# Ad-hoc WASM build using wasm-pack
|
||||
apps.wasm-build = {
|
||||
type = "app";
|
||||
program = "${pkgs.wasm-pack}/bin/wasm-pack";
|
||||
args = [
|
||||
"build"
|
||||
"wasm"
|
||||
"--target"
|
||||
"web"
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
2
wasm/.cargo/config.toml
Normal file
2
wasm/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||
40
wasm/Cargo.toml
Normal file
40
wasm/Cargo.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[package]
|
||||
name = "mtp-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"WebTransport",
|
||||
"WebTransportOptions",
|
||||
"WebTransportHash",
|
||||
"WebTransportBidirectionalStream",
|
||||
"WebTransportCloseInfo",
|
||||
"WebTransportDatagramDuplexStream",
|
||||
"WebTransportError",
|
||||
"WebTransportReceiveStream",
|
||||
"WebTransportSendStream",
|
||||
"ReadableStream",
|
||||
"ReadableStreamDefaultReader",
|
||||
"WritableStream",
|
||||
"WritableStreamDefaultWriter",
|
||||
] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
|
||||
getrandom = { version = "0.4", features = ["wasm_js"] }
|
||||
getrandom-v02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
||||
|
||||
mtp-common = { path = "../common" }
|
||||
mtp-type-map = { path = "../type-map" }
|
||||
mtp-codec = { path = "../codec", features = ["crypto"] }
|
||||
mtp-crypto = { path = "../crypto", features = ["wasm"] }
|
||||
316
wasm/src/client.rs
Normal file
316
wasm/src/client.rs
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
use std::cell::Cell;
|
||||
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_crypto::SignatureScheme;
|
||||
|
||||
use crate::error::js_error;
|
||||
use crate::transport::WasmTransport;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
Disconnected = 0,
|
||||
Connecting = 1,
|
||||
Connected = 2,
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ConnectionConfig {
|
||||
url: String,
|
||||
server_certificate_hashes: Option<Vec<String>>,
|
||||
client_id: u64,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ConnectionConfig {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(url: String) -> Self {
|
||||
Self { url, server_certificate_hashes: None, client_id: 0 }
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn url(&self) -> String { self.url.clone() }
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
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 }
|
||||
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_server_certificate_hashes(&mut self, hashes: Vec<String>) {
|
||||
self.server_certificate_hashes = Some(hashes);
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmClient {
|
||||
transport: Option<WasmTransport>,
|
||||
state: Rc<Cell<ConnectionState>>,
|
||||
on_state_change: js_sys::Function,
|
||||
pub(crate) on_message: js_sys::Function,
|
||||
pub(crate) on_error: js_sys::Function,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmClient {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
on_state_change: &js_sys::Function,
|
||||
on_message: &js_sys::Function,
|
||||
on_error: &js_sys::Function,
|
||||
) -> Self {
|
||||
Self {
|
||||
transport: None,
|
||||
state: Rc::new(Cell::new(ConnectionState::Disconnected)),
|
||||
on_state_change: on_state_change.clone(),
|
||||
on_message: on_message.clone(),
|
||||
on_error: on_error.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
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 }
|
||||
|
||||
/// 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).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));
|
||||
transport.send_frame(&ident.to_bytes()).await?;
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.set_state(ConnectionState::Connected);
|
||||
|
||||
let state = self.state.clone();
|
||||
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;
|
||||
state.set(ConnectionState::Disconnected);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Authenticated login with an existing client ID.
|
||||
/// Exchanges Identification + signatures and verifies the host response.
|
||||
///
|
||||
/// - `host_public_key_bytes`: serialized PublicKeyBundle from the server
|
||||
/// - `keyring_bytes`: serialized Keyring of this client (must match `client_id`)
|
||||
/// - `client_id`: previously assigned client ID
|
||||
///
|
||||
/// Returns the confirmed (same) client ID on success.
|
||||
#[wasm_bindgen]
|
||||
pub async fn auth_connect(
|
||||
&mut self,
|
||||
config: &ConnectionConfig,
|
||||
_host_public_key_bytes: &[u8],
|
||||
keyring_bytes: &[u8],
|
||||
client_id: u64,
|
||||
) -> Result<u64, JsValue> {
|
||||
self.set_state(ConnectionState::Connecting);
|
||||
|
||||
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"))?;
|
||||
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
||||
|
||||
// Build signature payload: version || client_id || client_nonce
|
||||
let mut sig_payload = Vec::new();
|
||||
sig_payload.extend_from_slice(version_str.as_bytes());
|
||||
sig_payload.extend_from_slice(&client_id.to_be_bytes());
|
||||
sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
|
||||
|
||||
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)
|
||||
.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::Signature, DataValue::Bytes(signature))
|
||||
.to_bytes();
|
||||
|
||||
let transport = WasmTransport::connect(&config.url).await?;
|
||||
let inner = transport.inner().clone();
|
||||
transport.send_frame(&frame).await?;
|
||||
|
||||
// Read and verify the host's IdentificationResponse
|
||||
let response = transport.read_one_frame().await?;
|
||||
let resp_comm = CommunicationValue::from_bytes(&response)
|
||||
.map_err(|e| js_error(&format!("parse response: {}", e)))?;
|
||||
|
||||
let resp_type = resp_comm.get_type();
|
||||
let expected_type = CommunicationTypeId(16); // IdentificationResponse
|
||||
if resp_type != expected_type {
|
||||
return Err(js_error("unexpected response type"));
|
||||
}
|
||||
|
||||
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
||||
return Err(js_error("host rejected authentication"));
|
||||
}
|
||||
|
||||
// Verify echoed nonce
|
||||
let echo_nonce = resp_comm.get_data(DataTypeId(7));
|
||||
if *echo_nonce != DataValue::UnsignedNumber(client_nonce) {
|
||||
return Err(js_error("nonce mismatch"));
|
||||
}
|
||||
|
||||
// Extract assigned ID
|
||||
let assigned_id = match resp_comm.get_data(DataTypeId(6)) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => return Err(js_error("missing assigned ID")),
|
||||
};
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.set_state(ConnectionState::Connected);
|
||||
|
||||
let state = self.state.clone();
|
||||
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;
|
||||
state.set(ConnectionState::Disconnected);
|
||||
});
|
||||
|
||||
Ok(assigned_id)
|
||||
}
|
||||
|
||||
/// Authenticated registration with a fresh keyring.
|
||||
/// The server assigns a new client ID.
|
||||
///
|
||||
/// - `host_public_key_bytes`: serialized PublicKeyBundle from the server
|
||||
/// - `keyring_bytes`: serialized Keyring (must include ed25519 secret key)
|
||||
///
|
||||
/// Returns the newly assigned client ID.
|
||||
#[wasm_bindgen]
|
||||
pub async fn auth_register(
|
||||
&mut self,
|
||||
config: &ConnectionConfig,
|
||||
host_public_key_bytes: &[u8],
|
||||
keyring_bytes: &[u8],
|
||||
) -> 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"))?;
|
||||
let client_nonce = u128::from_be_bytes(nonce_bytes);
|
||||
|
||||
let pk_bytes = keyring.public_key_bundle().as_bytes();
|
||||
|
||||
// Build signature payload: version || client_nonce || pk_bytes
|
||||
let mut sig_payload = Vec::new();
|
||||
sig_payload.extend_from_slice(version_str.as_bytes());
|
||||
sig_payload.extend_from_slice(&client_nonce.to_be_bytes());
|
||||
sig_payload.extend_from_slice(&pk_bytes);
|
||||
|
||||
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)
|
||||
.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::PublicKeys, DataValue::Bytes(pk_bytes))
|
||||
.add_typed_default(DataType::Signature, DataValue::Bytes(signature))
|
||||
.to_bytes();
|
||||
|
||||
let transport = WasmTransport::connect(&config.url).await?;
|
||||
let inner = transport.inner().clone();
|
||||
transport.send_frame(&frame).await?;
|
||||
|
||||
let response = transport.read_one_frame().await?;
|
||||
let resp_comm = CommunicationValue::from_bytes(&response)
|
||||
.map_err(|e| js_error(&format!("parse response: {}", e)))?;
|
||||
|
||||
let resp_type = resp_comm.get_type();
|
||||
let expected_type = CommunicationTypeId(18); // RegisterResponse
|
||||
if resp_type != expected_type {
|
||||
return Err(js_error("unexpected response type"));
|
||||
}
|
||||
|
||||
if resp_comm.get_data(DataTypeId(11)) != &DataValue::BoolTrue {
|
||||
return Err(js_error("host rejected registration"));
|
||||
}
|
||||
|
||||
let echo = resp_comm.get_data(DataTypeId(7));
|
||||
if *echo != DataValue::UnsignedNumber(client_nonce) {
|
||||
return Err(js_error("nonce mismatch"));
|
||||
}
|
||||
|
||||
let assigned_id = match resp_comm.get_data(DataTypeId(6)) {
|
||||
DataValue::UnsignedNumber(n) => *n as u64,
|
||||
_ => return Err(js_error("missing assigned ID")),
|
||||
};
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.set_state(ConnectionState::Connected);
|
||||
|
||||
let state = self.state.clone();
|
||||
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;
|
||||
state.set(ConnectionState::Disconnected);
|
||||
});
|
||||
|
||||
Ok(assigned_id)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn send(&self, frame: Vec<u8>) -> Result<(), JsValue> {
|
||||
match &self.transport {
|
||||
Some(t) => t.send_frame(&frame).await,
|
||||
None => Err(js_error("not connected")),
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn disconnect(&mut self) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
263
wasm/src/crypto.rs
Normal file
263
wasm/src/crypto.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_crypto::{
|
||||
AeadDecrypt, AeadEncrypt, Ed25519Signer, KemPrivateKey, KemPublicKey, Keyring,
|
||||
PublicKeyBundle, SignaturePqPrivateKey, SignaturePqPublicKey, SignaturePrivateKey,
|
||||
SignaturePublicKey, SignatureScheme, ChaCha20Poly1305, sha256, sha256_double,
|
||||
};
|
||||
|
||||
use crate::error::js_error;
|
||||
|
||||
// ===========================================================================
|
||||
// Keyring
|
||||
// ===========================================================================
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmKeyring {
|
||||
inner: Keyring,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmKeyring {
|
||||
/// Serialise the keyring to bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
/// 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)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/// Return the public half of this keyring as a bundle.
|
||||
#[wasm_bindgen]
|
||||
pub fn public_key_bundle(&self) -> WasmPublicKeyBundle {
|
||||
WasmPublicKeyBundle {
|
||||
inner: self.inner.public_key_bundle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a [`Keyring`] containing only an Ed25519 keypair (no KEM, no ML-DSA).
|
||||
///
|
||||
/// Takes the Ed25519 secret key and public key, each 32 bytes.
|
||||
/// Returns the serialised keyring bytes, suitable for passing to `WasmClient.auth_register`.
|
||||
#[wasm_bindgen]
|
||||
pub fn keyring_from_ed25519(secret_key: &[u8], public_key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
if secret_key.len() != 32 {
|
||||
return Err(js_error("ed25519 secret key must be 32 bytes"));
|
||||
}
|
||||
if public_key.len() != 32 {
|
||||
return Err(js_error("ed25519 public key must be 32 bytes"));
|
||||
}
|
||||
let keyring = Keyring::new(
|
||||
KemPublicKey::new(vec![]),
|
||||
KemPrivateKey::new(vec![]),
|
||||
SignaturePqPublicKey::new(vec![]),
|
||||
SignaturePqPrivateKey::new(vec![]),
|
||||
SignaturePublicKey::new(public_key.to_vec()),
|
||||
SignaturePrivateKey::new(secret_key.to_vec()),
|
||||
);
|
||||
Ok(keyring.to_bytes())
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// PublicKeyBundle
|
||||
// ===========================================================================
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmPublicKeyBundle {
|
||||
inner: PublicKeyBundle,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmPublicKeyBundle {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn kem_public_key(&self) -> Vec<u8> {
|
||||
self.inner.kem_public_key.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sig_cl_public_key(&self) -> Vec<u8> {
|
||||
self.inner.sig_cl_public_key.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sig_pq_public_key(&self) -> Vec<u8> {
|
||||
self.inner.sig_pq_public_key.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
self.inner.as_bytes()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<WasmPublicKeyBundle, JsValue> {
|
||||
let inner = PublicKeyBundle::from_bytes(bytes)
|
||||
.map_err(|e| js_error(&format!("PublicKeyBundle::from_bytes: {}", e)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// ChaCha20-Poly1305 AEAD
|
||||
// ===========================================================================
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmChaCha20Poly1305 {
|
||||
inner: ChaCha20Poly1305,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmChaCha20Poly1305 {
|
||||
/// Create a new cipher with a 32-byte key.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(key: Vec<u8>) -> Result<WasmChaCha20Poly1305, JsValue> {
|
||||
if key.len() != 32 {
|
||||
return Err(js_error("ChaCha20Poly1305 key must be 32 bytes"));
|
||||
}
|
||||
let mut k = [0u8; 32];
|
||||
k.copy_from_slice(&key);
|
||||
Ok(Self {
|
||||
inner: ChaCha20Poly1305::new(k),
|
||||
})
|
||||
}
|
||||
|
||||
/// Encrypt `plaintext` with `aad`.
|
||||
/// Returns `nonce || ciphertext`.
|
||||
#[wasm_bindgen]
|
||||
pub fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
self.inner
|
||||
.encrypt(plaintext, aad)
|
||||
.map_err(|e| js_error(&format!("encrypt failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Decrypt `nonce || ciphertext` with `aad`.
|
||||
#[wasm_bindgen]
|
||||
pub fn decrypt(&self, ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
self.inner
|
||||
.decrypt(ciphertext, aad)
|
||||
.map_err(|e| js_error(&format!("decrypt failed: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Ed25519 signatures
|
||||
// ===========================================================================
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmEd25519Signer {
|
||||
inner: Ed25519Signer,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmEd25519Signer {
|
||||
/// Load a signer from its 32-byte secret key.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(secret_key: Vec<u8>) -> Result<WasmEd25519Signer, JsValue> {
|
||||
let sk = SignaturePrivateKey::new(secret_key);
|
||||
let inner =
|
||||
Ed25519Signer::new(&sk).map_err(|e| js_error(&format!("Ed25519Signer::new: {}", e)))?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/// Sign `message` and return the signature bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
self.inner
|
||||
.sign(message)
|
||||
.map_err(|e| js_error(&format!("sign failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Verify `signature` against `message`.
|
||||
#[wasm_bindgen]
|
||||
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), JsValue> {
|
||||
self.inner
|
||||
.verify(message, signature)
|
||||
.map_err(|e| js_error(&format!("verify failed: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Ed25519 key generation helper
|
||||
// ===========================================================================
|
||||
|
||||
/// Generate a fresh Ed25519 keypair.
|
||||
///
|
||||
/// Returns `{ signer: WasmEd25519Signer, secretKey: Uint8Array, publicKey: Uint8Array }`.
|
||||
#[wasm_bindgen]
|
||||
pub fn ed25519_generate() -> Result<JsValue, JsValue> {
|
||||
let (_signer, sk, pk) = Ed25519Signer::generate();
|
||||
let obj = js_sys::Object::new();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&JsValue::from_str("signer"),
|
||||
&WasmEd25519Signer::new(sk.as_bytes().to_vec())?.into(),
|
||||
)
|
||||
.map_err(|_| js_error("failed to set signer"))?;
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&JsValue::from_str("secretKey"),
|
||||
&js_sys::Uint8Array::from(sk.as_bytes()),
|
||||
)
|
||||
.map_err(|_| js_error("failed to set secretKey"))?;
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&JsValue::from_str("publicKey"),
|
||||
&js_sys::Uint8Array::from(pk.as_bytes()),
|
||||
)
|
||||
.map_err(|_| js_error("failed to set publicKey"))?;
|
||||
Ok(obj.into())
|
||||
}
|
||||
|
||||
/// Standalone Ed25519 signature verification.
|
||||
#[wasm_bindgen]
|
||||
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)))
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Hashing
|
||||
// ===========================================================================
|
||||
|
||||
/// SHA-256 digest.
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_sha256(data: &[u8]) -> Vec<u8> {
|
||||
sha256(data).to_vec()
|
||||
}
|
||||
|
||||
/// Double SHA-256 (SHA-256 applied twice).
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_sha256_double(data: &[u8]) -> Vec<u8> {
|
||||
sha256_double(data).to_vec()
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// KDF
|
||||
// ===========================================================================
|
||||
|
||||
/// 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> {
|
||||
mtp_crypto::hkdf_expand(ikm, salt, info, len)
|
||||
.map_err(|e| js_error(&format!("hkdf_expand failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Derive a 32-byte encryption key from `ikm` with `salt` and `context`.
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_derive_encryption_key(
|
||||
ikm: &[u8],
|
||||
salt: &[u8],
|
||||
context: &[u8],
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
mtp_crypto::derive_encryption_key(ikm, salt, context)
|
||||
.map(|key| key.to_vec())
|
||||
.map_err(|e| js_error(&format!("derive_encryption_key failed: {}", e)))
|
||||
}
|
||||
17
wasm/src/error.rs
Normal file
17
wasm/src/error.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use wasm_bindgen::JsValue;
|
||||
|
||||
pub fn js_error(msg: impl Into<String>) -> JsValue {
|
||||
JsValue::from_str(&msg.into())
|
||||
}
|
||||
|
||||
pub fn from_codec_error(e: mtp_common::CodecError) -> JsValue {
|
||||
js_error(e.to_string())
|
||||
}
|
||||
|
||||
pub fn from_communication_error(e: mtp_common::CommunicationError) -> JsValue {
|
||||
js_error(e.to_string())
|
||||
}
|
||||
|
||||
pub fn from_crypto_error(e: mtp_crypto::CryptoError) -> JsValue {
|
||||
js_error(e.to_string())
|
||||
}
|
||||
13
wasm/src/lib.rs
Normal file
13
wasm/src/lib.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
pub mod error;
|
||||
pub mod transport;
|
||||
pub mod client;
|
||||
pub mod crypto;
|
||||
pub mod message;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
web_sys::console::log_1(&"mtp-wasm: module loaded".into());
|
||||
}
|
||||
135
wasm/src/message.rs
Normal file
135
wasm/src/message.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use mtp_codec::{
|
||||
CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue,
|
||||
};
|
||||
use mtp_crypto::{
|
||||
ChaCha20Poly1305, Ed25519Signer, Keyring, SigAlgorithm,
|
||||
derive_encryption_key,
|
||||
};
|
||||
|
||||
use crate::error::js_error;
|
||||
|
||||
/// Build a simple Ping frame with description, timestamp, and optional data.
|
||||
#[wasm_bindgen]
|
||||
pub fn build_ping_frame(
|
||||
client_id: u64,
|
||||
description: &str,
|
||||
timestamp: u64,
|
||||
data: &[u8],
|
||||
) -> Vec<u8> {
|
||||
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))
|
||||
.with_sender(client_id);
|
||||
|
||||
if !data.is_empty() {
|
||||
msg = msg.add_typed_default(DataType::Id, DataValue::Bytes(data.to_vec()));
|
||||
}
|
||||
|
||||
msg.to_bytes()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let keyring = Keyring::from_bytes(keyring_bytes)
|
||||
.map_err(|e| js_error(&format!("invalid keyring: {}", e)))?;
|
||||
|
||||
let enc_key = derive_encryption_key(
|
||||
b"MTP-demo-shared-secret",
|
||||
b"MTP-demo-salt",
|
||||
b"encrypted-container-demo",
|
||||
)
|
||||
.map_err(|e| js_error(&format!("key derivation failed: {}", e)))?;
|
||||
|
||||
let cipher = ChaCha20Poly1305::new(enc_key);
|
||||
let signer = Ed25519Signer::new(&keyring.sig_cl_secret_key)
|
||||
.map_err(|e| js_error(&format!("signer creation failed: {}", e)))?;
|
||||
|
||||
// Encrypted container (DataTypeId 1 = arbitrary custom)
|
||||
let inner_enc = DataValue::Container(vec![
|
||||
(DataTypeId(1), DataValue::Str("secret inner data".into())),
|
||||
(DataTypeId(2), DataValue::UnsignedNumber(42)),
|
||||
]);
|
||||
let mut dv_enc = inner_enc;
|
||||
dv_enc.encrypt_container(&cipher, b"demo-aad")
|
||||
.ok_or_else(|| js_error("encryption failed"))?;
|
||||
|
||||
// Signed container
|
||||
let inner_sig = DataValue::Container(vec![
|
||||
(DataTypeId(1), DataValue::Str("signed by client".into())),
|
||||
(DataTypeId(2), DataValue::UnsignedNumber(99)),
|
||||
]);
|
||||
let mut dv_sig = inner_sig;
|
||||
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(2), DataValue::UnsignedNumber(7)),
|
||||
]);
|
||||
let mut dv_sec = inner_sec;
|
||||
dv_sec.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, 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::Version, DataValue::Str("demo-wasm".into()))
|
||||
.with_sender(client_id);
|
||||
|
||||
Ok(msg.to_bytes())
|
||||
}
|
||||
|
||||
/// Parse an auth response frame into a JS object.
|
||||
#[wasm_bindgen]
|
||||
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(DataTypeId(11)), DataValue::BoolTrue);
|
||||
|
||||
let client_nonce = match comm.get_data(DataTypeId(7)) {
|
||||
DataValue::UnsignedNumber(n) => Some(*n),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let assigned_id = match comm.get_data(DataTypeId(6)) {
|
||||
DataValue::UnsignedNumber(n) => Some(*n as u64),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let timestamp = match comm.get_data(DataTypeId(5)) {
|
||||
DataValue::UnsignedNumber(n) => Some(*n),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let signature = match comm.get_data(DataTypeId(10)) {
|
||||
DataValue::Bytes(b) => Some(b.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let obj = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&obj, &"connected".into(), &JsValue::from(connected)).ok();
|
||||
if let Some(n) = client_nonce {
|
||||
let arr = js_sys::Uint8Array::from(&n.to_be_bytes()[..]);
|
||||
js_sys::Reflect::set(&obj, &"clientNonce".into(), &arr).ok();
|
||||
}
|
||||
if let Some(id) = assigned_id {
|
||||
js_sys::Reflect::set(&obj, &"assignedId".into(), &JsValue::from(id as f64)).ok();
|
||||
}
|
||||
if let Some(ts) = timestamp {
|
||||
js_sys::Reflect::set(&obj, &"timestamp".into(), &JsValue::from(ts as f64)).ok();
|
||||
}
|
||||
if let Some(sig) = signature {
|
||||
let arr = js_sys::Uint8Array::from(&sig[..]);
|
||||
js_sys::Reflect::set(&obj, &"signature".into(), &arr).ok();
|
||||
}
|
||||
|
||||
Ok(obj.into())
|
||||
}
|
||||
274
wasm/src/transport.rs
Normal file
274
wasm/src/transport.rs
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::WebTransport;
|
||||
|
||||
use crate::error::js_error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WasmTransport {
|
||||
inner: WebTransport,
|
||||
}
|
||||
|
||||
impl WasmTransport {
|
||||
pub async fn connect(url: &str) -> Result<Self, JsValue> {
|
||||
let transport = WebTransport::new(url)?;
|
||||
JsFuture::from(transport.ready()).await
|
||||
.map_err(|e| js_error(&format!("WebTransport ready failed: {:?}", e)))?;
|
||||
Ok(Self { inner: transport })
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &WebTransport {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn from_inner(inner: WebTransport) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub async fn send_frame(&self, frame: &[u8]) -> Result<(), JsValue> {
|
||||
let stream_promise = self.inner.create_unidirectional_stream();
|
||||
let stream = JsFuture::from(stream_promise).await?;
|
||||
|
||||
let writable = js_sys::Reflect::get(&stream, &JsValue::from_str("writable"))
|
||||
.map_err(|_| js_error("missing writable"))?;
|
||||
|
||||
let writer_val = js_sys::Reflect::get(&writable, &JsValue::from_str("getWriter"))
|
||||
.map_err(|_| js_error("missing getWriter"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("getWriter not a function"))?
|
||||
.call0(&writable)
|
||||
.map_err(|_| js_error("getWriter call failed"))?;
|
||||
|
||||
let len = frame.len() as u32;
|
||||
let mut wire = Vec::with_capacity(4 + frame.len());
|
||||
wire.extend_from_slice(&len.to_be_bytes());
|
||||
wire.extend_from_slice(frame);
|
||||
|
||||
let chunk = js_sys::Uint8Array::from(&wire[..]);
|
||||
|
||||
let write_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("write"))
|
||||
.map_err(|_| js_error("missing write"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("write not a function"))?;
|
||||
let write_promise = write_fn.call1(&writer_val, &chunk)
|
||||
.map_err(|e| js_error(&format!("write failed: {:?}", e)))?;
|
||||
JsFuture::from(write_promise.unchecked_into::<js_sys::Promise>()).await?;
|
||||
|
||||
let close_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("close"))
|
||||
.map_err(|_| js_error("missing close"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("close not a function"))?;
|
||||
let close_promise = close_fn.call0(&writer_val)
|
||||
.map_err(|e| js_error(&format!("close failed: {:?}", e)))?;
|
||||
JsFuture::from(close_promise.unchecked_into::<js_sys::Promise>()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read exactly one frame from incoming uni streams, then release the reader
|
||||
/// so `receive_loop` can pick up from where we left off.
|
||||
pub async fn read_one_frame(&self) -> Result<Vec<u8>, JsValue> {
|
||||
let incoming = self.inner.incoming_unidirectional_streams();
|
||||
|
||||
let reader_fn = js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader"))
|
||||
.map_err(|_| js_error("missing getReader"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("getReader not a function"))?;
|
||||
let reader_val = reader_fn.call0(&incoming)
|
||||
.map_err(|_| js_error("getReader call failed"))?;
|
||||
|
||||
let read_fn = js_sys::Reflect::get(&reader_val, &JsValue::from_str("read"))
|
||||
.map_err(|_| js_error("missing read"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("read not a function"))?;
|
||||
let result_promise = read_fn.call0(&reader_val)
|
||||
.map_err(|_| js_error("read call failed"))?;
|
||||
let result = JsFuture::from(result_promise.unchecked_into::<js_sys::Promise>()).await
|
||||
.map_err(|e| js_error(&format!("read failed: {:?}", e)))?;
|
||||
|
||||
// Release the reader lock so receive_loop can create its own reader
|
||||
if let Some(release_fn) = js_sys::Reflect::get(&reader_val, &JsValue::from_str("releaseLock"))
|
||||
.ok().and_then(|f| f.dyn_into::<js_sys::Function>().ok())
|
||||
{
|
||||
let _ = release_fn.call0(&reader_val);
|
||||
}
|
||||
|
||||
let done = js_sys::Reflect::get(&result, &JsValue::from_str("done"))
|
||||
.ok().and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
if done {
|
||||
return Err(js_error("stream ended before frame"));
|
||||
}
|
||||
|
||||
let recv_stream = js_sys::Reflect::get(&result, &JsValue::from_str("value"))
|
||||
.map_err(|_| js_error("missing value"))?;
|
||||
|
||||
let readable = js_sys::Reflect::get(&recv_stream, &JsValue::from_str("readable"))
|
||||
.map_err(|_| js_error("missing readable"))?;
|
||||
|
||||
let stream_reader_fn = js_sys::Reflect::get(&readable, &JsValue::from_str("getReader"))
|
||||
.map_err(|_| js_error("missing stream getReader"))?
|
||||
.dyn_into::<js_sys::Function>()
|
||||
.map_err(|_| js_error("stream getReader not a function"))?;
|
||||
let stream_reader = stream_reader_fn.call0(&readable)
|
||||
.map_err(|_| js_error("stream getReader call failed"))?;
|
||||
|
||||
let mut chunks: Vec<Vec<u8>> = Vec::new();
|
||||
loop {
|
||||
let stream_read_fn = match js_sys::Reflect::get(&stream_reader, &JsValue::from_str("read"))
|
||||
.ok().and_then(|f| f.dyn_into::<js_sys::Function>().ok())
|
||||
{
|
||||
Some(f) => f,
|
||||
None => break,
|
||||
};
|
||||
let chunk_promise = match stream_read_fn.call0(&stream_reader) {
|
||||
Ok(p) => p,
|
||||
Err(_) => break,
|
||||
};
|
||||
let chunk_result = match JsFuture::from(chunk_promise.unchecked_into::<js_sys::Promise>()).await {
|
||||
Ok(v) => v,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let chunk_done = js_sys::Reflect::get(&chunk_result, &JsValue::from_str("done"))
|
||||
.ok().and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
if chunk_done {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(chunk_val) = js_sys::Reflect::get(&chunk_result, &JsValue::from_str("value")) {
|
||||
let arr = js_sys::Uint8Array::new(&chunk_val).to_vec();
|
||||
if !arr.is_empty() {
|
||||
chunks.push(arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chunks.is_empty() {
|
||||
return Err(js_error("empty stream"));
|
||||
}
|
||||
|
||||
let total: usize = chunks.iter().map(|c| c.len()).sum();
|
||||
let mut buf = Vec::with_capacity(total);
|
||||
for c in &chunks {
|
||||
buf.extend_from_slice(c);
|
||||
}
|
||||
|
||||
if buf.len() < 4 {
|
||||
return Err(js_error("frame too short"));
|
||||
}
|
||||
let frame_len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||
if 4 + frame_len > buf.len() {
|
||||
return Err(js_error("frame truncated"));
|
||||
}
|
||||
|
||||
Ok(buf[4..4 + frame_len].to_vec())
|
||||
}
|
||||
|
||||
pub async fn receive_loop(
|
||||
&self,
|
||||
on_message: js_sys::Function,
|
||||
on_error: js_sys::Function,
|
||||
) {
|
||||
let incoming = self.inner.incoming_unidirectional_streams();
|
||||
|
||||
let reader_fn = match js_sys::Reflect::get(&incoming, &JsValue::from_str("getReader")) {
|
||||
Ok(f) => f.dyn_into::<js_sys::Function>().unwrap(),
|
||||
Err(_) => return,
|
||||
};
|
||||
let reader_val = match reader_fn.call0(&incoming) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
loop {
|
||||
let read_fn = match js_sys::Reflect::get(&reader_val, &JsValue::from_str("read")) {
|
||||
Ok(f) => f.dyn_into::<js_sys::Function>().unwrap(),
|
||||
Err(_) => break,
|
||||
};
|
||||
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,
|
||||
};
|
||||
|
||||
let done = js_sys::Reflect::get(&result, &JsValue::from_str("done"))
|
||||
.ok().and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
if done { break; }
|
||||
|
||||
let recv_stream = match js_sys::Reflect::get(&result, &JsValue::from_str("value")) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let readable = match js_sys::Reflect::get(&recv_stream, &JsValue::from_str("readable")) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let stream_reader_fn = match js_sys::Reflect::get(&readable, &JsValue::from_str("getReader")) {
|
||||
Ok(f) => f.dyn_into::<js_sys::Function>().unwrap(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
let stream_reader = match stream_reader_fn.call0(&readable) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let mut chunks: Vec<Vec<u8>> = Vec::new();
|
||||
loop {
|
||||
let stream_read_fn = match js_sys::Reflect::get(&stream_reader, &JsValue::from_str("read")) {
|
||||
Ok(f) => f.dyn_into::<js_sys::Function>().unwrap(),
|
||||
Err(_) => break,
|
||||
};
|
||||
let chunk_result = match stream_read_fn.call0(&stream_reader) {
|
||||
Ok(p) => match JsFuture::from(p.unchecked_into::<js_sys::Promise>()).await {
|
||||
Ok(v) => v,
|
||||
Err(_) => break,
|
||||
},
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let chunk_done = js_sys::Reflect::get(&chunk_result, &JsValue::from_str("done"))
|
||||
.ok().and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
if chunk_done { break; }
|
||||
|
||||
if let Ok(chunk_val) = js_sys::Reflect::get(&chunk_result, &JsValue::from_str("value")) {
|
||||
let arr = js_sys::Uint8Array::new(&chunk_val).to_vec();
|
||||
if !arr.is_empty() { chunks.push(arr); }
|
||||
}
|
||||
}
|
||||
|
||||
if chunks.is_empty() { continue; }
|
||||
|
||||
let total: usize = chunks.iter().map(|c| c.len()).sum();
|
||||
let mut buf = Vec::with_capacity(total);
|
||||
for c in &chunks { buf.extend_from_slice(c); }
|
||||
|
||||
if buf.len() < 4 {
|
||||
let _ = on_error.call1(&JsValue::NULL, &JsValue::from_str("frame too short"));
|
||||
continue;
|
||||
}
|
||||
let frame_len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||
if 4 + frame_len > buf.len() {
|
||||
let _ = on_error.call1(&JsValue::NULL, &JsValue::from_str("frame truncated"));
|
||||
continue;
|
||||
}
|
||||
let frame = buf[4..4 + frame_len].to_vec();
|
||||
let arr = js_sys::Uint8Array::from(&frame[..]);
|
||||
let _ = on_message.call1(&JsValue::NULL, &arr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let info = web_sys::WebTransportCloseInfo::new();
|
||||
let _ = self.inner.close_with_close_info(&info);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue