(wasm) Treat arbitrary byte data as Uint8Array, instead of base64url marshalling.

This commit is contained in:
Brandon Vandegrift 2023-09-20 00:46:45 -04:00
parent a7b073cddb
commit 80afa19678
17 changed files with 452 additions and 116 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -215,7 +215,6 @@ impl VeilidAPIError {
}
}
#[cfg_attr(target_arch = "wasm32", declare)]
pub type VeilidAPIResult<T> = Result<T, VeilidAPIError>;
impl From<std::io::Error> for VeilidAPIError {

View File

@ -9,9 +9,13 @@ pub struct VeilidAppMessage {
#[cfg_attr(target_arch = "wasm32", tsify(optional, type = "string"))]
sender: Option<TypedKey>,
#[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<u8>,
}
@ -40,8 +44,13 @@ pub struct VeilidAppCall {
#[cfg_attr(target_arch = "wasm32", tsify(optional))]
sender: Option<TypedKey>,
#[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<u8>,
#[serde(with = "as_human_string")]

View File

@ -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<u8>,
/// The public identity key of the writer of the data

View File

@ -1,9 +1,7 @@
use crate::*;
////////////////////////////////////////////////////////////////////////////////////////////////
#[cfg_attr(target_arch = "wasm32", declare)]
pub type ConfigCallbackReturn = VeilidAPIResult<Box<dyn core::any::Any + Send>>;
#[cfg_attr(target_arch = "wasm32", declare)]
pub type ConfigCallback = Arc<dyn Fn(String) -> ConfigCallbackReturn + Send + Sync>;
/// Enable and configure HTTPS access to the Veilid node

View File

@ -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"

View File

@ -58,7 +58,7 @@ impl VeilidCrypto {
APIResult::Ok(out.to_string())
}
pub fn randomBytes(kind: String, len: u32) -> APIResult<String> {
pub fn randomBytes(kind: String, len: u32) -> APIResult<Box<[u8]>> {
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<String> {
pub fn hashPassword(kind: String, password: Box<[u8]>, salt: Box<[u8]>) -> APIResult<String> {
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<bool> {
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<String> {
pub fn deriveSharedSecret(
kind: String,
password: Box<[u8]>,
salt: Box<[u8]>,
) -> APIResult<String> {
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<StringArray> {
let node_ids = into_unchecked_string_vec(node_ids);
@ -199,8 +198,6 @@ impl VeilidCrypto {
})
.collect::<APIResult<Vec<TypedKey>>>()?;
let data: Vec<u8> = unmarshall(data)?;
let typed_signatures = into_unchecked_string_vec(signatures);
let typed_signatures: Vec<TypedSignature> = typed_signatures
.iter()
@ -226,9 +223,7 @@ impl VeilidCrypto {
APIResult::Ok(out)
}
pub fn generateSignatures(data: String, key_pairs: StringArray) -> APIResult<StringArray> {
let data = unmarshall(data)?;
pub fn generateSignatures(data: Box<[u8]>, key_pairs: StringArray) -> APIResult<StringArray> {
let key_pairs = into_unchecked_string_vec(key_pairs);
let key_pairs: Vec<TypedKeyPair> = key_pairs
.iter()
@ -269,11 +264,9 @@ impl VeilidCrypto {
APIResult::Ok(out)
}
pub fn generateHash(kind: String, data: String) -> APIResult<String> {
pub fn generateHash(kind: String, data: Box<[u8]>) -> APIResult<String> {
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<bool> {
pub fn validateHash(kind: String, data: Box<[u8]>, hash: String) -> APIResult<bool> {
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<String> {
pub fn sign(kind: String, key: String, secret: String, data: Box<[u8]>) -> APIResult<String> {
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<String>,
) -> APIResult<String> {
associated_data: Option<Box<[u8]>>,
) -> APIResult<Box<[u8]>> {
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<String>,
) -> APIResult<String> {
associated_data: Option<Box<[u8]>>,
) -> APIResult<Box<[u8]>> {
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<Vec<u8>> = 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<String> {
) -> APIResult<Box<[u8]>> {
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)
}
}

View File

@ -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<String> {
let request: Vec<u8> = unmarshall(request)?;
pub async fn appCall(
&self,
target_string: String,
request: Box<[u8]>,
) -> APIResult<Uint8Array> {
let request: Vec<u8> = 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<Option<ValueData>> {
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?;

View File

@ -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<Option<String>> {
pub async fn load(&mut self, columnId: u32, key: Box<[u8]>) -> APIResult<Option<Uint8Array>> {
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<Option<String>> {
pub async fn delete(&mut self, columnId: u32, key: Box<[u8]>) -> APIResult<Option<Uint8Array>> {
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<StringArray> {
/// Returns an array of Uint8Array keys.
pub async fn getKeys(&mut self, columnId: u32) -> APIResult<Uint8ArrayArray> {
self.ensureOpen().await;
let table_db = self.getTableDB()?;
let keys = table_db.clone().get_keys(columnId).await?;
let out: Vec<String> = keys.into_iter().map(|k| marshall(&k)).collect();
let out = into_unchecked_string_array(out);
let out: Vec<Uint8Array> = 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)
}

View File

@ -37,6 +37,19 @@ pub(crate) fn into_unchecked_string_array(items: Vec<String>) -> StringArray {
.unchecked_into::<StringArray>() // TODO: can I do this a better way?
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "Uint8Array[]")]
pub type Uint8ArrayArray;
}
/// Convert a `Vec<Uint8Array>` into a `js_sys::Array` with the type of `Uint8Array[]`
pub(crate) fn into_unchecked_uint8array_array(items: Vec<Uint8Array>) -> Uint8ArrayArray {
items
.iter()
.collect::<js_sys::Array>()
.unchecked_into::<Uint8ArrayArray>() // TODO: can I do this a better way?
}
/// Convert a StringArray (`js_sys::Array` with the type of `string[]`) into `Vec<String>`
pub(crate) fn into_unchecked_string_vec(items: StringArray) -> Vec<String> {
items

View File

@ -1,2 +1,2 @@
node_modules
veilid-wasm-pkg
coverage

View File

@ -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();
});
});
});
});

View File

@ -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();
});

View File

@ -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) {

View File

@ -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();
});
});

View File

@ -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: {