Jack-Edwards / BlazorSodium

A small wrapper around libsodium.js for Blazor WASM
https://www.nuget.org/packages/BlazorSodium/
MIT License
9 stars 1 forks source link

Use "inner" / "lower-level" libsodium functions #34

Open Jack-Edwards opened 1 year ago

Jack-Edwards commented 1 year ago

Libsodium.js offers some nice wrappers to make using the library a lot simpler. However, using these wrappers means we are often copying data to and from JavaScript on every call.

It's possible to use MemoryView in .NET 7 to give JavaScript access to managed memory. For example:

C#

public static void MemoryViewTest()
{
   byte[] buffer = new byte[10];
   MemoryViewTest(buffer, 10);
}

[JSImport("memoryViewTest", "blazorSodium")]
public static partial void MemoryViewTest([JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> buffer, int length);

JS

export function memoryViewTest(arraySegment, length) {
   var copy = arraySegment.slice();
   copy[0]++;
   arraySegment.set(copy);
}

The buffer created in managed memory can be directly accessed and manipulated by JavaScript without having to copy the data.

One problem (among several) I'm facing is that the lower-level libsodium.js functions have extra, undocumented arguments. I opened a discussion topic for this on the main libsodium repo: https://github.com/jedisct1/libsodium/discussions/1222

Another problem I'm facing is libsodium.js rejects the raw arraySegment, as it is neither a Uint8Array nor a string. slice() should work to convert the data into an array that libsodium.js will accept, but that probably means I'll need to write a layer of JavaScript to interop between the C# calls and libsodium.js. Another option may be to create a fork of libsodium.js that tries to slice() the data on it's own, but I really want to avoid this.

Jack-Edwards commented 1 year ago

I have some basic examples of using the libsodium wrappers with MemoryView, ArraySegment, and Span.

[JSImport("randomBytes_MemoryViewTest", "blazorSodium")]
public static partial void RandomBytes_Buf_MemoryView_Test(int size, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> buffer);
export function randomBytes_MemoryViewTest(size, buffer) {
   buffer.set(sodium.randombytes_buf(size));
}

and

[JSImport("cryptoShortHashMemoryView", "blazorSodium")]
public static partial void Crypto_ShortHash_MemoryView_Test([JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> hashBuffer, [JSMarshalAs<JSType.MemoryView>] Span<byte> messageBuffer, [JSMarshalAs<JSType.MemoryView>] Span<byte> keyBuffer);
export function cryptoShortHashMemoryView(hashBuffer, messageBuffer, keyBuffer) {
   hashBuffer.set(sodium.crypto_shorthash(messageBuffer.slice(), keyBuffer.slice()));
}

Overall, performance actually takes a huge hit when using MemoryView. In cases where I was passing byte[] to a method which accepted ArraySegment<byte> and Span<byte>, the overhead to change the types on the fly is huge. It took 4x longer to process the same data.

The impact is less dramatic when you convert to ArraySegment<byte> before calling a method and keep that ArraySegment alive for multiple calls into libsodium. But performance is still worse.

The only case where I could find an improvement in performance is when doing something drastic, such as hashing a buffer containing 100_000 or more bytes 10_000 or more times. Even then, the tests only completed 5% faster. Really not worth the trouble.

Thought to be fair I've only tested on my own development computer, which is relatively powerful with good memory speeds.

Jack-Edwards commented 1 year ago

I still theorize there are significant performance gains to be had. Take this encryption example:

  1. Allocate a buffer in the .NET runtime
  2. Copy the buffer to JavaScript
  3. Malloc and copy the data to libsodium
  4. libsodium returns the data to JavaScript
  5. Copy the data to .NET, where another allocation occurs.

I can provide a MemoryView of ArraySegment instead, which behaves like a shared buffer between the runtime and JavaScript, but that still doesn't reduce any allocations within the libsodium environment.

The problem is I can't figure out how to use the base or "unwrapped" libsodium.js methods. These methods take buffer addresses instead of array instances. But whenever I provide an IntPtr from C# to these methods, I get an "index out of bounds" error from the runtime. Is the runtime guarding against whatever libsodium is doing to the managed memory space? Am I not allocating enough space in the buffer? Is libsodium only looking within it's own memory space?