diff --git a/wasm/.cargo/config.toml b/wasm/.cargo/config.toml index f5d9120..461a6af 100644 --- a/wasm/.cargo/config.toml +++ b/wasm/.cargo/config.toml @@ -1,2 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + [target.wasm32-unknown-unknown] rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index f83278c..3ac76f7 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -38,3 +38,7 @@ mtp-common = { path = "../common" } mtp-type-map = { path = "../type-map" } mtp-codec = { path = "../codec", features = ["crypto"] } mtp-crypto = { path = "../crypto", features = ["wasm"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" +hex = "0.4" diff --git a/wasm/src/crypto.rs b/wasm/src/crypto.rs index 21f4082..401fabe 100644 --- a/wasm/src/crypto.rs +++ b/wasm/src/crypto.rs @@ -261,3 +261,218 @@ pub fn wasm_derive_encryption_key( .map(|key| key.to_vec()) .map_err(|e| js_error(&format!("derive_encryption_key failed: {}", e))) } + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + // ------------------------------------------------------------------ + // Keyring + // ------------------------------------------------------------------ + + #[wasm_bindgen_test] + fn keyring_from_ed25519_roundtrip() { + let sk = vec![0xabu8; 32]; + let pk = vec![0x42u8; 32]; + let bytes = keyring_from_ed25519(&sk, &pk).expect("keyring_from_ed25519 failed"); + + let restored = WasmKeyring::from_bytes(&bytes).expect("from_bytes failed"); + let bundle = restored.public_key_bundle(); + assert_eq!(bundle.sig_cl_public_key(), pk); + } + + #[wasm_bindgen_test] + fn keyring_from_ed25519_wrong_key_length() { + let short = vec![0u8; 16]; + let ok = vec![0u8; 32]; + assert!(keyring_from_ed25519(&short, &ok).is_err()); + assert!(keyring_from_ed25519(&ok, &short).is_err()); + } + + // ------------------------------------------------------------------ + // PublicKeyBundle + // ------------------------------------------------------------------ + + #[wasm_bindgen_test] + fn public_key_bundle_roundtrip() { + let pk = vec![0x99u8; 32]; + let bundle = WasmPublicKeyBundle { + inner: PublicKeyBundle { + kem_public_key: KemPublicKey::new(vec![1, 2, 3]), + sig_cl_public_key: SignaturePublicKey::new(pk.clone()), + sig_pq_public_key: SignaturePqPublicKey::new(vec![4, 5, 6]), + }, + }; + + let bytes = bundle.to_bytes(); + let restored = WasmPublicKeyBundle::from_bytes(&bytes).expect("from_bytes failed"); + assert_eq!(restored.sig_cl_public_key(), pk); + } + + // ------------------------------------------------------------------ + // ChaCha20-Poly1305 + // ------------------------------------------------------------------ + + #[wasm_bindgen_test] + fn chacha20_encrypt_decrypt_roundtrip() { + let key = vec![0x42u8; 32]; + let cipher = WasmChaCha20Poly1305::new(key).expect("new failed"); + + let plaintext = b"hello wasm crypto"; + let aad = b"test-aad"; + + let encrypted = cipher.encrypt(plaintext, aad).expect("encrypt failed"); + let decrypted = cipher.decrypt(&encrypted, aad).expect("decrypt failed"); + + assert_eq!(decrypted, plaintext); + } + + #[wasm_bindgen_test] + fn chacha20_wrong_key_length() { + assert!(WasmChaCha20Poly1305::new(vec![0u8; 16]).is_err()); + assert!(WasmChaCha20Poly1305::new(vec![0u8; 31]).is_err()); + assert!(WasmChaCha20Poly1305::new(vec![0u8; 33]).is_err()); + } + + #[wasm_bindgen_test] + fn chacha20_decrypt_wrong_key_fails() { + let key1 = vec![0x42u8; 32]; + let key2 = vec![0x43u8; 32]; + let cipher1 = WasmChaCha20Poly1305::new(key1).expect("new failed"); + let cipher2 = WasmChaCha20Poly1305::new(key2).expect("new failed"); + + let encrypted = cipher1.encrypt(b"secret", b"aad").expect("encrypt failed"); + let result = cipher2.decrypt(&encrypted, b"aad"); + assert!(result.is_err()); + } + + // ------------------------------------------------------------------ + // Ed25519 + // ------------------------------------------------------------------ + + #[wasm_bindgen_test] + fn ed25519_sign_verify() { + let (_signer, sk, _pk) = Ed25519Signer::generate(); + + let signer = WasmEd25519Signer::new(sk.as_bytes().to_vec()).expect("new failed"); + let message = b"test message for ed25519"; + + let signature = signer.sign(message).expect("sign failed"); + assert!(!signature.is_empty()); + + signer.verify(message, &signature).expect("verify failed"); + } + + #[wasm_bindgen_test] + fn ed25519_sign_wrong_message_fails_verify() { + let (_signer, sk, _pk) = Ed25519Signer::generate(); + + let signer = WasmEd25519Signer::new(sk.as_bytes().to_vec()).expect("new failed"); + let signature = signer.sign(b"message A").expect("sign failed"); + + let result = signer.verify(b"message B", &signature); + assert!(result.is_err()); + } + + #[wasm_bindgen_test] + fn ed25519_generate_returns_valid() { + let result = ed25519_generate().expect("generate failed"); + + let has_signer = js_sys::Reflect::has(&result, &"signer".into()).unwrap_or(false); + let has_sk = js_sys::Reflect::has(&result, &"secretKey".into()).unwrap_or(false); + let has_pk = js_sys::Reflect::has(&result, &"publicKey".into()).unwrap_or(false); + + assert!(has_signer); + assert!(has_sk); + assert!(has_pk); + } + + #[wasm_bindgen_test] + fn ed25519_verify_standalone() { + let (_signer, sk, pk) = Ed25519Signer::generate(); + let signer = WasmEd25519Signer::new(sk.as_bytes().to_vec()).expect("new failed"); + let msg = b"standalone verify test"; + let sig = signer.sign(msg).expect("sign failed"); + + ed25519_verify(pk.as_bytes().to_vec(), msg, &sig).expect("verify failed"); + } + + #[wasm_bindgen_test] + fn ed25519_verify_bad_signature_fails() { + let pk = vec![0x42u8; 32]; + let msg = b"test"; + let bad_sig = vec![0x00u8; 64]; + let result = ed25519_verify(pk, msg, &bad_sig); + assert!(result.is_err()); + } + + // ------------------------------------------------------------------ + // Hashing + // ------------------------------------------------------------------ + + #[wasm_bindgen_test] + fn sha256_empty() { + let result = wasm_sha256(b""); + // SHA-256 of empty string + 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"); + assert_eq!(result, expected); + } + + #[wasm_bindgen_test] + fn sha256_double() { + let single = wasm_sha256(b"test"); + let double = wasm_sha256_double(b"test"); + let expected = wasm_sha256(&single); + assert_eq!(double, expected); + } + + // ------------------------------------------------------------------ + // HKDF / KDF + // ------------------------------------------------------------------ + + #[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"); + assert_eq!(result.len(), 32); + } + + #[wasm_bindgen_test] + fn hkdf_expand_different_info() { + let r1 = wasm_hkdf_expand(b"ikm", b"salt", b"info1", 16).expect("hkdf failed"); + let r2 = wasm_hkdf_expand(b"ikm", b"salt", b"info2", 16).expect("hkdf failed"); + assert_ne!(r1, r2); + } + + #[wasm_bindgen_test] + fn derive_encryption_key_roundtrip() { + 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"); + 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"); + assert_ne!(key, key2); + } +} diff --git a/wasm/src/error.rs b/wasm/src/error.rs index c4dc93f..c3ed596 100644 --- a/wasm/src/error.rs +++ b/wasm/src/error.rs @@ -15,3 +15,48 @@ pub fn from_communication_error(e: mtp_common::CommunicationError) -> JsValue { pub fn from_crypto_error(e: mtp_crypto::CryptoError) -> JsValue { js_error(e.to_string()) } + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn js_error_contains_message() { + let err = js_error("test error message"); + assert_eq!(err.as_string(), Some("test error message".to_string())); + } + + #[wasm_bindgen_test] + fn js_error_from_string() { + let err = js_error(String::from("owned string error")); + assert_eq!(err.as_string(), Some("owned string error".to_string())); + } + + #[wasm_bindgen_test] + fn from_codec_error_invalid_encoding() { + let err = from_codec_error(mtp_common::CodecError::InvalidEncoding); + assert!(err.as_string().unwrap().contains("Invalid encoding")); + } + + #[wasm_bindgen_test] + fn from_codec_error_unknown_version() { + let err = from_codec_error(mtp_common::CodecError::UnknownVersion); + assert!(err.as_string().unwrap().contains("Unknown version")); + } + + #[wasm_bindgen_test] + fn from_communication_error_renders() { + use mtp_common::CommunicationError; + let err = from_communication_error(CommunicationError::ConnectionLost); + assert!(err.as_string().unwrap().contains("Connection terminated")); + } + + #[wasm_bindgen_test] + fn from_crypto_error_renders() { + use mtp_crypto::CryptoError; + let err = from_crypto_error(CryptoError::InvalidKeyLength); + assert!(err.as_string().unwrap().contains("invalid key length")); + } +} diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index bb4ef3d..183730d 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -1,11 +1,12 @@ -pub mod error; -pub mod transport; pub mod client; pub mod crypto; +pub mod error; pub mod message; +pub mod transport; use wasm_bindgen::prelude::*; +#[cfg(not(test))] #[wasm_bindgen(start)] pub fn main() { console_error_panic_hook::set_once(); diff --git a/wasm/src/message.rs b/wasm/src/message.rs index 0d17646..0212cd9 100644 --- a/wasm/src/message.rs +++ b/wasm/src/message.rs @@ -1,7 +1,7 @@ use wasm_bindgen::prelude::*; use mtp_codec::{ - CommunicationType, CommunicationValue, DataType, DataTypeId, DataValue, + CommunicationType, CommunicationTypeId, CommunicationValue, DataType, DataTypeId, DataValue, }; use mtp_crypto::{ ChaCha20Poly1305, Ed25519Signer, Keyring, SigAlgorithm, @@ -133,3 +133,130 @@ pub fn parse_auth_response(response: &[u8]) -> Result { Ok(obj.into()) } + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn build_ping_frame_roundtrip() { + let bytes = build_ping_frame(42, "test-ping", 1234567890, &[]); + let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + + 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)); + } + + #[wasm_bindgen_test] + fn build_ping_frame_with_data() { + let payload = b"attachment-data"; + let bytes = build_ping_frame(99, "with-data", 555, payload); + let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + + 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(5)), &DataValue::UnsignedNumber(555)); + assert_eq!(cv.get_data(DataTypeId(6)), &DataValue::Bytes(payload.to_vec())); + } + + #[wasm_bindgen_test] + fn build_ping_frame_client_id_zero() { + let bytes = build_ping_frame(0, "zero-id", 0, &[]); + let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + assert_eq!(cv.get_sender(), 0); + } + + #[wasm_bindgen_test] + fn build_demo_message_roundtrip() { + let (_signer, sk, pk) = Ed25519Signer::generate(); + let keyring = Keyring::new( + mtp_crypto::KemPublicKey::new(vec![]), + mtp_crypto::KemPrivateKey::new(vec![]), + mtp_crypto::SignaturePqPublicKey::new(vec![]), + mtp_crypto::SignaturePqPrivateKey::new(vec![]), + pk, + sk, + ); + let keyring_bytes = keyring.to_bytes(); + + let result = build_demo_message(7, &keyring_bytes); + assert!(result.is_ok()); + + let bytes = result.unwrap(); + let cv = CommunicationValue::from_bytes(&bytes).expect("decode failed"); + + 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())); + } + + #[wasm_bindgen_test] + fn build_demo_message_invalid_keyring() { + let result = build_demo_message(1, b"not-a-valid-keyring"); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.as_string().unwrap().contains("invalid keyring")); + } + + #[wasm_bindgen_test] + fn parse_auth_response_success() { + let resp = CommunicationValue::new(CommunicationType::IdentificationResponse) + .add_typed_default(DataType::Connected, DataValue::BoolTrue) + .add_typed_default(DataType::ClientNonce, DataValue::UnsignedNumber(999)) + .add_typed_default(DataType::Id, DataValue::UnsignedNumber(42)) + .add_typed_default(DataType::Timestamp, DataValue::UnsignedNumber(12345)) + .to_bytes(); + + 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()); + assert_eq!(connected, Some(true)); + + let id = js_sys::Reflect::get(&result, &"assignedId".into()) + .ok().and_then(|v| v.as_f64()); + assert_eq!(id, Some(42.0)); + } + + #[wasm_bindgen_test] + fn parse_auth_response_rejected() { + let resp = CommunicationValue::new(CommunicationType::IdentificationResponse) + .add_typed_default(DataType::Connected, DataValue::BoolFalse) + .to_bytes(); + + 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()); + assert_eq!(connected, Some(false)); + + // rejected should have no assignedId + let has_id = js_sys::Reflect::has(&result, &"assignedId".into()).unwrap_or(false); + assert!(!has_id); + } + + #[wasm_bindgen_test] + fn parse_auth_response_with_signature() { + let sig_bytes = vec![0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe]; + let resp = CommunicationValue::new(CommunicationType::IdentificationResponse) + .add_typed_default(DataType::Connected, DataValue::BoolTrue) + .add_typed_default(DataType::Signature, DataValue::Bytes(sig_bytes.clone())) + .to_bytes(); + + let result = parse_auth_response(&resp).expect("parse failed"); + + let has_sig = js_sys::Reflect::has(&result, &"signature".into()).unwrap_or(false); + assert!(has_sig); + } + + #[wasm_bindgen_test] + fn parse_auth_response_invalid_frame() { + let result = parse_auth_response(b"garbage-data"); + assert!(result.is_err()); + } +}