From 80afa19678d6ecb61b8dfef5e99837eaa4822568 Mon Sep 17 00:00:00 2001 From: Brandon Vandegrift <798832-bmv437@users.noreply.gitlab.com> Date: Wed, 20 Sep 2023 00:46:45 -0400 Subject: [PATCH] (wasm) Treat arbitrary byte data as Uint8Array, instead of base64url marshalling. --- Cargo.lock | 11 ++ veilid-core/Cargo.toml | 3 + veilid-core/src/veilid_api/error.rs | 1 - .../src/veilid_api/types/app_message_call.rs | 15 +- .../src/veilid_api/types/dht/value_data.rs | 8 +- veilid-core/src/veilid_config.rs | 2 - veilid-wasm/Cargo.toml | 3 + veilid-wasm/src/veilid_crypto_js.rs | 84 +++------ veilid-wasm/src/veilid_routing_context_js.rs | 33 ++-- veilid-wasm/src/veilid_table_db_js.rs | 38 ++-- veilid-wasm/src/wasm_helpers.rs | 13 ++ veilid-wasm/tests/.gitignore | 2 +- .../tests/src/VeilidRoutingContext.test.ts | 173 ++++++++++++++++++ veilid-wasm/tests/src/VeilidTable.test.ts | 36 +++- .../tests/src/utils/marshalling-utils.ts | 20 +- veilid-wasm/tests/src/veilidCrypto.test.ts | 111 ++++++++++- veilid-wasm/tests/wdio.conf.ts | 15 +- 17 files changed, 452 insertions(+), 116 deletions(-) create mode 100644 veilid-wasm/tests/src/VeilidRoutingContext.test.ts diff --git a/Cargo.lock b/Cargo.lock index d210e639..098a244a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4156,6 +4156,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_bytes" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +dependencies = [ + "serde", +] + [[package]] name = "serde_cbor" version = "0.11.2" @@ -5316,6 +5325,7 @@ dependencies = [ "serde", "serde-big-array", "serde-wasm-bindgen 0.6.0", + "serde_bytes", "serde_json", "serial_test", "shell-words", @@ -5523,6 +5533,7 @@ dependencies = [ "send_wrapper 0.6.0", "serde", "serde-wasm-bindgen 0.6.0", + "serde_bytes", "serde_json", "tracing", "tracing-subscriber", diff --git a/veilid-core/Cargo.toml b/veilid-core/Cargo.toml index 7778c64c..b82eaf28 100644 --- a/veilid-core/Cargo.toml +++ b/veilid-core/Cargo.toml @@ -198,6 +198,9 @@ wasm-bindgen = "0.2.87" js-sys = "0.3.64" wasm-bindgen-futures = "0.4.37" send_wrapper = { version = "0.6.0", features = ["futures"] } +serde_bytes = { version = "0.11", default_features = false, features = [ + "alloc", +] } tsify = { version = "0.4.5", features = ["js"] } serde-wasm-bindgen = "0.6.0" diff --git a/veilid-core/src/veilid_api/error.rs b/veilid-core/src/veilid_api/error.rs index d6999118..39261179 100644 --- a/veilid-core/src/veilid_api/error.rs +++ b/veilid-core/src/veilid_api/error.rs @@ -215,7 +215,6 @@ impl VeilidAPIError { } } -#[cfg_attr(target_arch = "wasm32", declare)] pub type VeilidAPIResult = Result; impl From for VeilidAPIError { diff --git a/veilid-core/src/veilid_api/types/app_message_call.rs b/veilid-core/src/veilid_api/types/app_message_call.rs index 5af5b692..ff117e5e 100644 --- a/veilid-core/src/veilid_api/types/app_message_call.rs +++ b/veilid-core/src/veilid_api/types/app_message_call.rs @@ -9,9 +9,13 @@ pub struct VeilidAppMessage { #[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))] sender: Option, - #[serde(with = "as_human_base64")] + #[cfg_attr(not(target_arch = "wasm32"), serde(with = "as_human_base64"))] #[schemars(with = "String")] - #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] + #[cfg_attr( + target_arch = "wasm32", + serde(with = "serde_bytes"), + tsify(type = "Uint8Array") + )] message: Vec, } @@ -40,8 +44,13 @@ pub struct VeilidAppCall { #[cfg_attr(target_arch = "wasm32", tsify(optional))] sender: Option, - #[serde(with = "as_human_base64")] + #[cfg_attr(not(target_arch = "wasm32"), serde(with = "as_human_base64"))] #[schemars(with = "String")] + #[cfg_attr( + target_arch = "wasm32", + serde(with = "serde_bytes"), + tsify(type = "Uint8Array") + )] message: Vec, #[serde(with = "as_human_string")] diff --git a/veilid-core/src/veilid_api/types/dht/value_data.rs b/veilid-core/src/veilid_api/types/dht/value_data.rs index e43b9b2b..f89a1856 100644 --- a/veilid-core/src/veilid_api/types/dht/value_data.rs +++ b/veilid-core/src/veilid_api/types/dht/value_data.rs @@ -8,9 +8,13 @@ pub struct ValueData { seq: ValueSeqNum, /// The contents of a DHT Record - #[serde(with = "as_human_base64")] + #[cfg_attr(not(target_arch = "wasm32"), serde(with = "as_human_base64"))] #[schemars(with = "String")] - #[cfg_attr(target_arch = "wasm32", tsify(type = "string"))] + #[cfg_attr( + target_arch = "wasm32", + serde(with = "serde_bytes"), + tsify(type = "Uint8Array") + )] data: Vec, /// The public identity key of the writer of the data diff --git a/veilid-core/src/veilid_config.rs b/veilid-core/src/veilid_config.rs index 2877eea6..505151b6 100644 --- a/veilid-core/src/veilid_config.rs +++ b/veilid-core/src/veilid_config.rs @@ -1,9 +1,7 @@ use crate::*; //////////////////////////////////////////////////////////////////////////////////////////////// -#[cfg_attr(target_arch = "wasm32", declare)] pub type ConfigCallbackReturn = VeilidAPIResult>; -#[cfg_attr(target_arch = "wasm32", declare)] pub type ConfigCallback = Arc ConfigCallbackReturn + Send + Sync>; /// Enable and configure HTTPS access to the Veilid node diff --git a/veilid-wasm/Cargo.toml b/veilid-wasm/Cargo.toml index 59e46aa8..63d98e90 100644 --- a/veilid-wasm/Cargo.toml +++ b/veilid-wasm/Cargo.toml @@ -28,6 +28,9 @@ cfg-if = "^1" wasm-bindgen-futures = "^0" js-sys = "^0" serde_json = "^1" +serde_bytes = { version = "0.11", default_features = false, features = [ + "alloc", +] } serde = "^1" lazy_static = "^1" send_wrapper = "^0" diff --git a/veilid-wasm/src/veilid_crypto_js.rs b/veilid-wasm/src/veilid_crypto_js.rs index 55f4557d..21012046 100644 --- a/veilid-wasm/src/veilid_crypto_js.rs +++ b/veilid-wasm/src/veilid_crypto_js.rs @@ -58,7 +58,7 @@ impl VeilidCrypto { APIResult::Ok(out.to_string()) } - pub fn randomBytes(kind: String, len: u32) -> APIResult { + pub fn randomBytes(kind: String, len: u32) -> APIResult> { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; let veilid_api = get_veilid_api()?; @@ -71,7 +71,7 @@ impl VeilidCrypto { ) })?; let out = crypto_system.random_bytes(len); - let out = data_encoding::BASE64URL_NOPAD.encode(&out); + let out = out.into_boxed_slice(); APIResult::Ok(out) } @@ -91,10 +91,8 @@ impl VeilidCrypto { APIResult::Ok(out) } - pub fn hashPassword(kind: String, password: String, salt: String) -> APIResult { + pub fn hashPassword(kind: String, password: Box<[u8]>, salt: Box<[u8]>) -> APIResult { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let password = unmarshall(password)?; - let salt = unmarshall(salt)?; let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; @@ -111,11 +109,10 @@ impl VeilidCrypto { pub fn verifyPassword( kind: String, - password: String, + password: Box<[u8]>, password_hash: String, ) -> APIResult { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let password = unmarshall(password)?; let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; @@ -130,10 +127,12 @@ impl VeilidCrypto { APIResult::Ok(out) } - pub fn deriveSharedSecret(kind: String, password: String, salt: String) -> APIResult { + pub fn deriveSharedSecret( + kind: String, + password: Box<[u8]>, + salt: Box<[u8]>, + ) -> APIResult { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let password = unmarshall(password)?; - let salt = unmarshall(salt)?; let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; @@ -182,7 +181,7 @@ impl VeilidCrypto { pub fn verifySignatures( node_ids: StringArray, - data: String, + data: Box<[u8]>, signatures: StringArray, ) -> VeilidAPIResult { let node_ids = into_unchecked_string_vec(node_ids); @@ -199,8 +198,6 @@ impl VeilidCrypto { }) .collect::>>()?; - let data: Vec = unmarshall(data)?; - let typed_signatures = into_unchecked_string_vec(signatures); let typed_signatures: Vec = typed_signatures .iter() @@ -226,9 +223,7 @@ impl VeilidCrypto { APIResult::Ok(out) } - pub fn generateSignatures(data: String, key_pairs: StringArray) -> APIResult { - let data = unmarshall(data)?; - + pub fn generateSignatures(data: Box<[u8]>, key_pairs: StringArray) -> APIResult { let key_pairs = into_unchecked_string_vec(key_pairs); let key_pairs: Vec = key_pairs .iter() @@ -269,11 +264,9 @@ impl VeilidCrypto { APIResult::Ok(out) } - pub fn generateHash(kind: String, data: String) -> APIResult { + pub fn generateHash(kind: String, data: Box<[u8]>) -> APIResult { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let data = unmarshall(data)?; - let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; let crypto_system = crypto.get(kind).ok_or_else(|| { @@ -306,11 +299,9 @@ impl VeilidCrypto { APIResult::Ok(out) } - pub fn validateHash(kind: String, data: String, hash: String) -> APIResult { + pub fn validateHash(kind: String, data: Box<[u8]>, hash: String) -> APIResult { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let data = unmarshall(data)?; - let hash: veilid_core::HashDigest = veilid_core::HashDigest::from_str(&hash)?; let veilid_api = get_veilid_api()?; @@ -345,14 +336,12 @@ impl VeilidCrypto { APIResult::Ok(out.to_string()) } - pub fn sign(kind: String, key: String, secret: String, data: String) -> APIResult { + pub fn sign(kind: String, key: String, secret: String, data: Box<[u8]>) -> APIResult { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; let secret: veilid_core::SecretKey = veilid_core::SecretKey::from_str(&secret)?; - let data = unmarshall(data)?; - let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; let crypto_system = crypto.get(kind).ok_or_else(|| { @@ -362,11 +351,10 @@ impl VeilidCrypto { APIResult::Ok(out.to_string()) } - pub fn verify(kind: String, key: String, data: String, signature: String) -> APIResult<()> { + pub fn verify(kind: String, key: String, data: Box<[u8]>, signature: String) -> APIResult<()> { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; let key: veilid_core::PublicKey = veilid_core::PublicKey::from_str(&key)?; - let data = unmarshall(data)?; let signature: veilid_core::Signature = veilid_core::Signature::from_str(&signature)?; let veilid_api = get_veilid_api()?; @@ -396,24 +384,18 @@ impl VeilidCrypto { pub fn decryptAead( kind: String, - body: String, + body: Box<[u8]>, nonce: String, shared_secret: String, - associated_data: Option, - ) -> APIResult { + associated_data: Option>, + ) -> APIResult> { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let body = unmarshall(body)?; - let nonce: veilid_core::Nonce = veilid_core::Nonce::from_str(&nonce)?; let shared_secret: veilid_core::SharedSecret = veilid_core::SharedSecret::from_str(&shared_secret)?; - let associated_data = associated_data - .map(unmarshall) - .map_or(APIResult::Ok(None), |r| r.map(Some))?; - let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; let crypto_system = crypto.get(kind).ok_or_else(|| { @@ -428,34 +410,28 @@ impl VeilidCrypto { &nonce, &shared_secret, match &associated_data { - Some(ad) => Some(ad.as_slice()), + Some(ad) => Some(ad), None => None, }, )?; - let out = data_encoding::BASE64URL_NOPAD.encode(&out); + let out = out.into_boxed_slice(); APIResult::Ok(out) } pub fn encryptAead( kind: String, - body: String, + body: Box<[u8]>, nonce: String, shared_secret: String, - associated_data: Option, - ) -> APIResult { + associated_data: Option>, + ) -> APIResult> { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let body = unmarshall(body)?; - let nonce: veilid_core::Nonce = veilid_core::Nonce::from_str(&nonce)?; let shared_secret: veilid_core::SharedSecret = veilid_core::SharedSecret::from_str(&shared_secret)?; - let associated_data: Option> = associated_data - .map(unmarshall) - .map_or(APIResult::Ok(None), |r| r.map(Some))?; - let veilid_api = get_veilid_api()?; let crypto = veilid_api.crypto()?; let crypto_system = crypto.get(kind).ok_or_else(|| { @@ -470,24 +446,21 @@ impl VeilidCrypto { &nonce, &shared_secret, match &associated_data { - Some(ad) => Some(ad.as_slice()), + Some(ad) => Some(ad), None => None, }, )?; - let out = data_encoding::BASE64URL_NOPAD.encode(&out); - APIResult::Ok(out) + APIResult::Ok(out.into_boxed_slice()) } pub fn cryptNoAuth( kind: String, - body: String, + mut body: Box<[u8]>, nonce: String, shared_secret: String, - ) -> APIResult { + ) -> APIResult> { let kind: veilid_core::CryptoKind = veilid_core::FourCC::from_str(&kind)?; - let mut body = unmarshall(body)?; - let nonce: veilid_core::Nonce = veilid_core::Nonce::from_str(&nonce)?; let shared_secret: veilid_core::SharedSecret = @@ -503,7 +476,6 @@ impl VeilidCrypto { ) })?; crypto_system.crypt_in_place_no_auth(&mut body, &nonce, &shared_secret); - let out = data_encoding::BASE64URL_NOPAD.encode(&body); - APIResult::Ok(out) + APIResult::Ok(body) } } diff --git a/veilid-wasm/src/veilid_routing_context_js.rs b/veilid-wasm/src/veilid_routing_context_js.rs index 9707c0be..edfd82bc 100644 --- a/veilid-wasm/src/veilid_routing_context_js.rs +++ b/veilid-wasm/src/veilid_routing_context_js.rs @@ -83,8 +83,8 @@ impl VeilidRoutingContext { /// /// * `call_id` - specifies which call to reply to, and it comes from a VeilidUpdate::AppCall, specifically the VeilidAppCall::id() value. /// * `message` - is an answer blob to be returned by the remote node's RoutingContext::app_call() function, and may be up to 32768 bytes - pub async fn appCallReply(call_id: String, message: String) -> APIResult<()> { - let message = unmarshall(message)?; + pub async fn appCallReply(call_id: String, message: Box<[u8]>) -> APIResult<()> { + let message = message.into_vec(); let call_id = match call_id.parse() { Ok(v) => v, Err(e) => { @@ -148,10 +148,9 @@ impl VeilidRoutingContext { /// @param {string} target - can be either a direct node id or a private route. /// @param {string} message - an arbitrary message blob of up to `32768` bytes. #[wasm_bindgen(skip_jsdoc)] - pub async fn appMessage(&self, target_string: String, message: String) -> APIResult<()> { + pub async fn appMessage(&self, target_string: String, message: Box<[u8]>) -> APIResult<()> { let routing_context = self.getRoutingContext()?; - let message = unmarshall(message)?; - + let message = message.into_vec(); let veilid_api = get_veilid_api()?; let target = veilid_api.parse_as_target(target_string).await?; routing_context.app_message(target, message).await?; @@ -162,18 +161,22 @@ impl VeilidRoutingContext { /// /// Veilid apps may use this for arbitrary message passing. /// - /// @param {string} target_string - can be either a direct node id or a private route, base64Url encoded. - /// @param {string} message - an arbitrary message blob of up to `32768` bytes, base64Url encoded. - /// @returns an answer blob of up to `32768` bytes, base64Url encoded. + /// @param {string} target_string - can be either a direct node id or a private route. + /// @param {Uint8Array} message - an arbitrary message blob of up to `32768` bytes. + /// @returns {Uint8Array} an answer blob of up to `32768` bytes. #[wasm_bindgen(skip_jsdoc)] - pub async fn appCall(&self, target_string: String, request: String) -> APIResult { - let request: Vec = unmarshall(request)?; + pub async fn appCall( + &self, + target_string: String, + request: Box<[u8]>, + ) -> APIResult { + let request: Vec = request.into_vec(); let routing_context = self.getRoutingContext()?; let veilid_api = get_veilid_api()?; let target = veilid_api.parse_as_target(target_string).await?; let answer = routing_context.app_call(target, request).await?; - let answer = marshall(&answer); + let answer = Uint8Array::from(answer.as_slice()); APIResult::Ok(answer) } @@ -250,7 +253,7 @@ impl VeilidRoutingContext { /// May pull the latest value from the network, but by settings 'force_refresh' you can force a network data refresh. /// /// Returns `undefined` if the value subkey has not yet been set. - /// Returns base64Url encoded `data` if the value subkey has valid data. + /// Returns a Uint8Array of `data` if the value subkey has valid data. pub async fn getDhtValue( &self, key: String, @@ -268,15 +271,15 @@ impl VeilidRoutingContext { /// Pushes a changed subkey value to the network /// /// Returns `undefined` if the value was successfully put. - /// Returns base64Url encoded `data` if the value put was older than the one available on the network. + /// Returns a Uint8Array of `data` if the value put was older than the one available on the network. pub async fn setDhtValue( &self, key: String, subKey: u32, - data: String, + data: Box<[u8]>, ) -> APIResult> { let key = TypedKey::from_str(&key)?; - let data = unmarshall(data)?; + let data = data.into_vec(); let routing_context = self.getRoutingContext()?; let res = routing_context.set_dht_value(key, subKey, data).await?; diff --git a/veilid-wasm/src/veilid_table_db_js.rs b/veilid-wasm/src/veilid_table_db_js.rs index 181a43ef..1120fab5 100644 --- a/veilid-wasm/src/veilid_table_db_js.rs +++ b/veilid-wasm/src/veilid_table_db_js.rs @@ -63,22 +63,24 @@ impl VeilidTableDB { } /// Read a key from a column in the TableDB immediately. - pub async fn load(&mut self, columnId: u32, key: String) -> APIResult> { + pub async fn load(&mut self, columnId: u32, key: Box<[u8]>) -> APIResult> { self.ensureOpen().await; - let key = unmarshall(key)?; let table_db = self.getTableDB()?; let out = table_db.load(columnId, &key).await?; - let out = out.map(|out| marshall(&out)); + let out = out.map(|out| Uint8Array::from(out.as_slice())); APIResult::Ok(out) } /// Store a key with a value in a column in the TableDB. /// Performs a single transaction immediately. - pub async fn store(&mut self, columnId: u32, key: String, value: String) -> APIResult<()> { + pub async fn store( + &mut self, + columnId: u32, + key: Box<[u8]>, + value: Box<[u8]>, + ) -> APIResult<()> { self.ensureOpen().await; - let key = unmarshall(key)?; - let value = unmarshall(value)?; let table_db = self.getTableDB()?; table_db.store(columnId, &key, &value).await?; @@ -86,26 +88,29 @@ impl VeilidTableDB { } /// Delete key with from a column in the TableDB. - pub async fn delete(&mut self, columnId: u32, key: String) -> APIResult> { + pub async fn delete(&mut self, columnId: u32, key: Box<[u8]>) -> APIResult> { self.ensureOpen().await; - let key = unmarshall(key)?; let table_db = self.getTableDB()?; let out = table_db.delete(columnId, &key).await?; - let out = out.map(|out| marshall(&out)); + let out = out.map(|out| Uint8Array::from(out.as_slice())); APIResult::Ok(out) } /// Get the list of keys in a column of the TableDB. /// - /// Returns an array of base64Url encoded keys. - pub async fn getKeys(&mut self, columnId: u32) -> APIResult { + /// Returns an array of Uint8Array keys. + pub async fn getKeys(&mut self, columnId: u32) -> APIResult { self.ensureOpen().await; let table_db = self.getTableDB()?; let keys = table_db.clone().get_keys(columnId).await?; - let out: Vec = keys.into_iter().map(|k| marshall(&k)).collect(); - let out = into_unchecked_string_array(out); + let out: Vec = keys + .into_iter() + .map(|k| Uint8Array::from(k.as_slice())) + .collect(); + + let out = into_unchecked_uint8array_array(out); APIResult::Ok(out) } @@ -164,16 +169,13 @@ impl VeilidTableDBTransaction { /// Store a key with a value in a column in the TableDB. /// Does not modify TableDB until `.commit()` is called. - pub fn store(&self, col: u32, key: String, value: String) -> APIResult<()> { - let key = unmarshall(key)?; - let value = unmarshall(value)?; + pub fn store(&self, col: u32, key: Box<[u8]>, value: Box<[u8]>) -> APIResult<()> { let transaction = self.getTransaction()?; transaction.store(col, &key, &value) } /// Delete key with from a column in the TableDB - pub fn deleteKey(&self, col: u32, key: String) -> APIResult<()> { - let key = unmarshall(key)?; + pub fn deleteKey(&self, col: u32, key: Box<[u8]>) -> APIResult<()> { let transaction = self.getTransaction()?; transaction.delete(col, &key) } diff --git a/veilid-wasm/src/wasm_helpers.rs b/veilid-wasm/src/wasm_helpers.rs index 84a52069..d3b65e16 100644 --- a/veilid-wasm/src/wasm_helpers.rs +++ b/veilid-wasm/src/wasm_helpers.rs @@ -37,6 +37,19 @@ pub(crate) fn into_unchecked_string_array(items: Vec) -> StringArray { .unchecked_into::() // TODO: can I do this a better way? } +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Uint8Array[]")] + pub type Uint8ArrayArray; +} +/// Convert a `Vec` into a `js_sys::Array` with the type of `Uint8Array[]` +pub(crate) fn into_unchecked_uint8array_array(items: Vec) -> Uint8ArrayArray { + items + .iter() + .collect::() + .unchecked_into::() // TODO: can I do this a better way? +} + /// Convert a StringArray (`js_sys::Array` with the type of `string[]`) into `Vec` pub(crate) fn into_unchecked_string_vec(items: StringArray) -> Vec { items diff --git a/veilid-wasm/tests/.gitignore b/veilid-wasm/tests/.gitignore index 62b8e8fd..3091757a 100644 --- a/veilid-wasm/tests/.gitignore +++ b/veilid-wasm/tests/.gitignore @@ -1,2 +1,2 @@ node_modules -veilid-wasm-pkg \ No newline at end of file +coverage \ No newline at end of file diff --git a/veilid-wasm/tests/src/VeilidRoutingContext.test.ts b/veilid-wasm/tests/src/VeilidRoutingContext.test.ts new file mode 100644 index 00000000..325bfa3f --- /dev/null +++ b/veilid-wasm/tests/src/VeilidRoutingContext.test.ts @@ -0,0 +1,173 @@ +import { expect } from '@wdio/globals'; + +import { + veilidCoreInitConfig, + veilidCoreStartupConfig, +} from './utils/veilid-config'; + +import { + DHTRecordDescriptor, + VeilidRoutingContext, + veilidClient, + veilidCrypto, +} from 'veilid-wasm'; +import { textEncoder, textDecoder } from './utils/marshalling-utils'; +import { waitForMs } from './utils/wait-utils'; + +describe('VeilidRoutingContext', () => { + before('veilid startup', async () => { + veilidClient.initializeCore(veilidCoreInitConfig); + await veilidClient.startupCore((_update) => { + // if (_update.kind === 'Log') { + // console.log(_update.message); + // } + }, JSON.stringify(veilidCoreStartupConfig)); + await veilidClient.attach(); + await waitForMs(2000); + }); + + after('veilid shutdown', async () => { + await veilidClient.detach(); + await veilidClient.shutdownCore(); + }); + + describe('constructors', () => { + it('should create using .create()', async () => { + const routingContext = VeilidRoutingContext.create(); + expect(routingContext instanceof VeilidRoutingContext).toBe(true); + + routingContext.free(); + }); + + it('should create using new', async () => { + const routingContext = new VeilidRoutingContext(); + expect(routingContext instanceof VeilidRoutingContext).toBe(true); + + routingContext.free(); + }); + + it('should create with privacy', async () => { + const routingContext = VeilidRoutingContext.create().withPrivacy(); + expect(routingContext instanceof VeilidRoutingContext).toBe(true); + + routingContext.free(); + }); + + it('should create with custom privacy', async () => { + const routingContext = VeilidRoutingContext.create().withCustomPrivacy({ + Safe: { + hop_count: 2, + sequencing: 'EnsureOrdered', + stability: 'Reliable', + }, + }); + expect(routingContext instanceof VeilidRoutingContext).toBe(true); + + routingContext.free(); + }); + + it('should create with sequencing', async () => { + const routingContext = + VeilidRoutingContext.create().withSequencing('EnsureOrdered'); + expect(routingContext instanceof VeilidRoutingContext).toBe(true); + + routingContext.free(); + }); + }); + + describe('operations', () => { + let routingContext: VeilidRoutingContext; + + before('create routing context', () => { + routingContext = VeilidRoutingContext.create() + .withPrivacy() + .withSequencing('EnsureOrdered'); + }); + + after('free routing context', () => { + routingContext.free(); + }); + + describe('DHT kitchen sink', async () => { + let dhtRecord: DHTRecordDescriptor; + const data = '๐Ÿš€ This example DHT data with unicode a ฤ€ ๐€€ ๆ–‡ ๐Ÿš€'; + + before('create dht record', async () => { + const bestKind = veilidCrypto.bestCryptoKind(); + dhtRecord = await routingContext.createDhtRecord( + { + kind: 'DFLT', + o_cnt: 1, + }, + bestKind + ); + + expect(dhtRecord.key).toBeDefined(); + expect(dhtRecord.owner).toBeDefined(); + expect(dhtRecord.owner_secret).toBeDefined(); + expect(dhtRecord.schema).toEqual({ kind: 'DFLT', o_cnt: 1 }); + }); + + after('free dht record', async () => { + await routingContext.closeDhtRecord(dhtRecord.key); + }); + + it('should set value', async () => { + const setValueRes = await routingContext.setDhtValue( + dhtRecord.key, + 0, + textEncoder.encode(data) + ); + expect(setValueRes).toBeUndefined(); + }); + + it('should get value with force refresh', async () => { + const getValueRes = await routingContext.getDhtValue( + dhtRecord.key, + 0, + true + ); + expect(getValueRes?.data).toBeDefined(); + expect(textDecoder.decode(getValueRes?.data)).toBe(data); + + expect(getValueRes?.writer).toBe(dhtRecord.owner); + expect(getValueRes?.seq).toBe(0); + }); + + it('should open readonly record', async () => { + await routingContext.closeDhtRecord(dhtRecord.key); + + const readonlyDhtRecord = await routingContext.openDhtRecord( + dhtRecord.key + ); + expect(readonlyDhtRecord).toBeDefined(); + + const setValueRes = routingContext.setDhtValue( + dhtRecord.key, + 0, + textEncoder.encode(data) + ); + await expect(setValueRes).rejects.toEqual({ + kind: 'Generic', + message: 'value is not writable', + }); + }); + + it('should open writable record', async () => { + await routingContext.closeDhtRecord(dhtRecord.key); + + const writeableDhtRecord = await routingContext.openDhtRecord( + dhtRecord.key, + `${dhtRecord.owner}:${dhtRecord.owner_secret}` + ); + expect(writeableDhtRecord).toBeDefined(); + const setValueRes = await routingContext.setDhtValue( + dhtRecord.key, + 0, + textEncoder.encode(`${data}๐Ÿ‘‹`) + ); + expect(setValueRes).toBeUndefined(); + }); + }); + }); +}); diff --git a/veilid-wasm/tests/src/VeilidTable.test.ts b/veilid-wasm/tests/src/VeilidTable.test.ts index 68a81f0b..a157ff9c 100644 --- a/veilid-wasm/tests/src/VeilidTable.test.ts +++ b/veilid-wasm/tests/src/VeilidTable.test.ts @@ -6,7 +6,7 @@ import { } from './utils/veilid-config'; import { VeilidTableDB, veilidClient } from 'veilid-wasm'; -import { marshall, unmarshall } from './utils/marshalling-utils'; +import { textEncoder, textDecoder } from './utils/marshalling-utils'; const TABLE_NAME = 'some-table'; const TABLE_COLS = 1; @@ -57,18 +57,22 @@ describe('VeilidTable', () => { const value = 'test value with unicode ๐Ÿš€'; it('should store value', async () => { - await table.store(0, marshall(key), marshall(value)); + await table.store( + 0, + textEncoder.encode(key), + textEncoder.encode(value) + ); }); it('should load value', async () => { - const storedValue = await table.load(0, marshall(key)); + const storedValue = await table.load(0, textEncoder.encode(key)); expect(storedValue).toBeDefined(); - expect(unmarshall(storedValue!)).toBe(value); + expect(textDecoder.decode(storedValue!)).toBe(value); }); it('should have key in list of keys', async () => { const keys = await table.getKeys(0); - const decodedKeys = keys.map(unmarshall); + const decodedKeys = keys.map((key) => textDecoder.decode(key)); expect(decodedKeys).toEqual([key]); }); }); @@ -82,15 +86,27 @@ describe('VeilidTable', () => { const second = 'secondโœ”'; const third = 'third๐Ÿ“ข'; - transaction.store(0, marshall(key), marshall(first)); - transaction.store(0, marshall(key), marshall(second)); - transaction.store(0, marshall(key), marshall(third)); + transaction.store( + 0, + textEncoder.encode(key), + textEncoder.encode(first) + ); + transaction.store( + 0, + textEncoder.encode(key), + textEncoder.encode(second) + ); + transaction.store( + 0, + textEncoder.encode(key), + textEncoder.encode(third) + ); await transaction.commit(); - const storedValue = await table.load(0, marshall(key)); + const storedValue = await table.load(0, textEncoder.encode(key)); expect(storedValue).toBeDefined(); - expect(unmarshall(storedValue!)).toBe(third); + expect(textDecoder.decode(storedValue!)).toBe(third); transaction.free(); }); diff --git a/veilid-wasm/tests/src/utils/marshalling-utils.ts b/veilid-wasm/tests/src/utils/marshalling-utils.ts index 3adfd64e..94eff599 100644 --- a/veilid-wasm/tests/src/utils/marshalling-utils.ts +++ b/veilid-wasm/tests/src/utils/marshalling-utils.ts @@ -1,13 +1,23 @@ -// TextEncoder/TextDecoder are used to solve for "The Unicode Problem" https://stackoverflow.com/a/30106551 +export const textDecoder = new TextDecoder(); +export const textEncoder = new TextEncoder(); -export function marshall(data: string) { - const byteString = bytesToString(new TextEncoder().encode(data)); +// TextEncoder/TextDecoder are used to solve for "The Unicode Problem" https://stackoverflow.com/a/30106551 +export function marshallString(data: string) { + return marshallBytes(textEncoder.encode(data)); +} + +export function unmarshallString(b64: string) { + return textDecoder.decode(unmarshallBytes(b64)); +} + +export function marshallBytes(data: Uint8Array) { + const byteString = bytesToString(data); return base64UrlEncode(byteString); } -export function unmarshall(b64: string) { +export function unmarshallBytes(b64: string) { const byteString = base64UrlDecode(b64); - return new TextDecoder().decode(stringToBytes(byteString)); + return stringToBytes(byteString); } function base64UrlEncode(data: string) { diff --git a/veilid-wasm/tests/src/veilidCrypto.test.ts b/veilid-wasm/tests/src/veilidCrypto.test.ts index e2f37574..afbdfac2 100644 --- a/veilid-wasm/tests/src/veilidCrypto.test.ts +++ b/veilid-wasm/tests/src/veilidCrypto.test.ts @@ -6,6 +6,7 @@ import { } from './utils/veilid-config'; import { veilidClient, veilidCrypto } from 'veilid-wasm'; +import { textEncoder, unmarshallBytes } from './utils/marshalling-utils'; describe('veilidCrypto', () => { before('veilid startup', async () => { @@ -29,10 +30,116 @@ describe('veilidCrypto', () => { expect(kinds.includes(bestKind)).toBe(true); }); - it('should generate key pair', async () => { + it('should generate key pair', () => { const bestKind = veilidCrypto.bestCryptoKind(); const keypair = veilidCrypto.generateKeyPair(bestKind); expect(typeof keypair).toBe('string'); - // TODO: fix TypeScript return type of generateKeyPair to return string instead of KeyPair + + const [publicKey, secretKey] = keypair.split(':'); + expect(unmarshallBytes(publicKey).length).toBe(32); + expect(unmarshallBytes(secretKey).length).toBe(32); + + const isValid = veilidCrypto.validateKeyPair( + bestKind, + publicKey, + secretKey + ); + expect(isValid).toBe(true); }); + + it('should generate random bytes', () => { + const bestKind = veilidCrypto.bestCryptoKind(); + const bytes = veilidCrypto.randomBytes(bestKind, 64); + expect(bytes instanceof Uint8Array).toBe(true); + expect(bytes.length).toBe(64); + }); + + it('should hash data and validate hash', () => { + const bestKind = veilidCrypto.bestCryptoKind(); + const data = textEncoder.encode('this is my data๐Ÿš€'); + const hash = veilidCrypto.generateHash(bestKind, data); + + expect(hash).toBeDefined(); + expect(typeof hash).toBe('string'); + + const isValid = veilidCrypto.validateHash(bestKind, data, hash); + expect(isValid).toBe(true); + }); + + it('should hash and validate password', () => { + const bestKind = veilidCrypto.bestCryptoKind(); + + const password = textEncoder.encode('this is my data๐Ÿš€'); + const saltLength = veilidCrypto.defaultSaltLength(bestKind); + expect(saltLength).toBeGreaterThan(0); + + const salt = veilidCrypto.randomBytes(bestKind, saltLength); + expect(salt instanceof Uint8Array).toBe(true); + expect(salt.length).toBe(saltLength); + + const hash = veilidCrypto.hashPassword(bestKind, password, salt); + expect(hash).toBeDefined(); + expect(typeof hash).toBe('string'); + + const isValid = veilidCrypto.verifyPassword(bestKind, password, hash); + expect(isValid).toBe(true); + }); + + it('should aead encrypt and decrypt', () => { + const bestKind = veilidCrypto.bestCryptoKind(); + const body = textEncoder.encode( + 'This is an encoded body with my secret data in it๐Ÿ”ฅ' + ); + const ad = textEncoder.encode( + 'This is data associated with my secret data๐Ÿ‘‹' + ); + + const nonce = veilidCrypto.randomNonce(bestKind); + expect(typeof nonce).toBe('string'); + + const sharedSecred = veilidCrypto.randomSharedSecret(bestKind); + expect(typeof sharedSecred).toBe('string'); + + const encBody = veilidCrypto.encryptAead( + bestKind, + body, + nonce, + sharedSecred, + ad + ); + expect(encBody instanceof Uint8Array).toBe(true); + + const overhead = veilidCrypto.aeadOverhead(bestKind); + expect(encBody.length - body.length).toBe(overhead); + + const decBody = veilidCrypto.decryptAead( + bestKind, + encBody, + nonce, + sharedSecred, + ad + ); + expect(decBody instanceof Uint8Array).toBe(true); + expect(body).toEqual(decBody); + }); + + it('should sign and verify', () => { + const bestKind = veilidCrypto.bestCryptoKind(); + const keypair = veilidCrypto.generateKeyPair(bestKind); + const data = textEncoder.encode( + 'This is some data I am signing with my key ๐Ÿ”‘' + ); + expect(typeof keypair).toBe('string'); + + const [publicKey, secretKey] = keypair.split(':'); + + const sig = veilidCrypto.sign(bestKind, publicKey, secretKey, data); + expect(typeof sig).toBe('string'); + + expect(() => { + const res = veilidCrypto.verify(bestKind, publicKey, data, sig); + expect(res).toBeUndefined(); + }).not.toThrow(); + }); + }); diff --git a/veilid-wasm/tests/wdio.conf.ts b/veilid-wasm/tests/wdio.conf.ts index 200ba2b8..d9d38a74 100644 --- a/veilid-wasm/tests/wdio.conf.ts +++ b/veilid-wasm/tests/wdio.conf.ts @@ -5,7 +5,20 @@ export const config: Options.Testrunner = { // Runner Configuration // ==================== // WebdriverIO supports running e2e tests as well as unit and component tests. - runner: ['browser', { viteConfig: './vite.config.ts' }], + runner: [ + 'browser', + { + viteConfig: './vite.config.ts', + coverage: { + enabled: true, + // needed since the ../pkg directory that has the compiled wasm npm package + // is outside the current directory. Coverage is only collected on files + // that are in within `cwd`. + cwd: '..', + include: ['pkg/**'], + }, + }, + ], autoCompileOpts: { autoCompile: true, tsNodeOpts: {