(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

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