Website Logo Light Website Logo Dark OUTWIT

WitRPC and Blazor WebAssembly

Published on: 1/27/2025


Blazor WebAssembly is a powerful tool for creating web frontends and beyond when integrated with frameworks such as MAUI, WinUI, or WPF. By pairing Blazor WebAssembly with UI frameworks like MudBlazor, developers can create sophisticated web applications using a unified C# object model, without needing extensive knowledge of HTML, CSS, or JavaScript.

However, Blazor WebAssembly comes with specific considerations, particularly around security and encryption.

Encryption in Blazor WebAssembly with WitRPC

Blazor WebAssembly operates within the browser environment, restricting the use of .NET’s built-in cryptography libraries (System.Security.Cryptography). Therefore, developers using WitRPC have two encryption options:

Option 1: Disable Built-in Encryption

Client-side configuration:

var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket($"ws://localhost:{PORT}/webSocket/");
options.WithoutEncryptor();
options.WithJson();
options.WithLogger(logger);
options.WithTimeout(TimeSpan.FromSeconds(1));
});

Server-side configuration:

var server = WitServerBuilder.Build(options =>
{
options.WithService(service);
options.WithWebSocket(url, MAX_CLIENTS);
options.WithoutEncryption();
options.WithJson();
options.WithTimeout(TimeSpan.FromSeconds(1));
options.WithLogger(logger);
});

Option 2: Implement a Custom Encryptor

WitRPC supports custom encryption implementations compatible with Blazor WebAssembly through JavaScript interop, leveraging the browser’s Web Crypto API. Below is a JavaScript implementation (cryptoInterop.js):

window.cryptoInterop = {
privateKey: null,
publicKey: null,
async generateKeys(keySize) {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: keySize,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);
this.privateKey = keyPair.privateKey;
this.publicKey = keyPair.publicKey;
},
async getPublicKey() {
const exported = await window.crypto.subtle.exportKey("jwk", this.publicKey);
return JSON.stringify(exported);
},
async getPrivateKey() {
const exported = await window.crypto.subtle.exportKey("jwk", this.privateKey);
return JSON.stringify(exported);
},
async decryptRSA(encryptedBase64) {
const encryptedData = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0)).buffer;
const decrypted = await window.crypto.subtle.decrypt(
{
name: "RSA-OAEP"
},
this.privateKey,
encryptedData
);
const decryptedBase64 = btoa(String.fromCharCode(...new Uint8Array(decrypted)));
return decryptedBase64;
},
async encryptAes(base64Key, base64Iv, base64Data) {
const keyBytes = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
const ivBytes = Uint8Array.from(atob(base64Iv), c => c.charCodeAt(0));
const dataBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
const key = await window.crypto.subtle.importKey(
"raw",
keyBytes,
{
name: "AES-CBC"
},
false,
["encrypt"]
);
const encrypted = await window.crypto.subtle.encrypt(
{
name: "AES-CBC",
iv: ivBytes
},
key,
dataBytes
);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
},
async decryptAes(base64Key, base64Iv, base64EncryptedData) {
const keyBytes = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
const ivBytes = Uint8Array.from(atob(base64Iv), c => c.charCodeAt(0));
const encryptedBytes = Uint8Array.from(atob(base64EncryptedData), c => c.charCodeAt(0));
const key = await window.crypto.subtle.importKey(
"raw",
keyBytes,
{
name: "AES-CBC"
},
false,
["decrypt"]
);
const decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-CBC",
iv: ivBytes
},
key,
encryptedBytes
);
return btoa(String.fromCharCode(...new Uint8Array(decrypted)));
}
};

The corresponding C# class using IJSRuntime:

public class EncryptorClientWeb : IEncryptorClient
{
public EncryptorClientWeb(IJSRuntime jsRuntime)
{
Runtime = jsRuntime;
}
public async Task<bool> InitAsync()
{
if (IsInitialized)
return true;
try
{
await Runtime.InvokeVoidAsync("cryptoInterop.generateKeys", 2048);
var options = new JsonSerializerOptions
{
Converters = { new DualNameJsonConverter<RSAParametersWeb>() }
};
var publicKeyString = await Runtime.InvokeAsync<string>("cryptoInterop.getPublicKey");
var publicKey = JsonSerializer.Deserialize<RSAParametersWeb>(publicKeyString, options);
var publicKeyJson = JsonSerializer.Serialize(publicKey, options);
PublicKey = Encoding.UTF8.GetBytes(publicKeyJson);
var privateKeyString = await Runtime.InvokeAsync<string>("cryptoInterop.getPrivateKey");
var privateKey = JsonSerializer.Deserialize<RSAParametersWeb>(privateKeyString, options);
var privateKeyJson = JsonSerializer.Serialize(privateKey, options);
PrivateKey = Encoding.UTF8.GetBytes(privateKeyJson);
IsInitialized = true;
return true;
}
catch (Exception e)
{
return false;
}
}
public byte[] GetPublicKey() => PublicKey ?? new byte[0];
public byte[] GetPrivateKey() => PrivateKey ?? new byte[0];
public async Task<byte[]> DecryptRsa(byte[] data)
{
var result = await Runtime.InvokeAsync<string>("cryptoInterop.decryptRSA", Convert.ToBase64String(data));
return Convert.FromBase64String(result.Base64UrlToBase64());
}
public bool ResetAes(byte[] symmetricKey, byte[] vector)
{
try
{
AesKey = Convert.ToBase64String(symmetricKey);
AesIv = Convert.ToBase64String(vector);
return true;
}
catch (Exception e)
{
return false;
}
}
public async Task<byte[]> Encrypt(byte[] data)
{
var result = await Runtime.InvokeAsync<string>("cryptoInterop.encryptAes", AesKey, AesIv, Convert.ToBase64String(data));
return Convert.FromBase64String(result.Base64UrlToBase64());
}
public async Task<byte[]> Decrypt(byte[] data)
{
var result = await Runtime.InvokeAsync<string>("cryptoInterop.decryptAes", AesKey, AesIv, Convert.ToBase64String(data));
return Convert.FromBase64String(result.Base64UrlToBase64());
}
public void Dispose() { }
public bool IsInitialized { get; private set; }
public IJSRuntime Runtime { get; }
private byte[]? PublicKey { get; set; }
private byte[]? PrivateKey { get; set; }
private string? AesKey { get; set; }
private string? AesIv { get; set; }
}

To use this custom encryptor:

  1. Register it with the service collection:
builder.Services.AddScoped<EncryptorClientWeb>();
  1. Inject it into components or services:
[Inject] public EncryptorClientWeb Encryptor { get; private set; }
  1. Pass it to WitRPC when building the client:
var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket($"ws://localhost:{PORT}/webSocket/");
options.WithEncryptor(Encryptor);
options.WithJson();
options.WithLogger(logger);
options.WithTimeout(TimeSpan.FromSeconds(1));
});

Blazor WebAssembly and AoT

By default, Blazor WebAssembly uses Just-In-Time (JIT) compilation, enabling dynamic features like dynamic proxies. However, enabling Ahead-of-Time (AoT) compilation improves performance but restricts the use of dynamic proxies.

To enable AoT:

<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

With AoT enabled, switch to static proxies in WitRPC:

var service = client.GetService<IExampleService>(x => new ExampleServiceProxy(x), false);

WitRPC isn’t limited to desktop or service-based .NET applications. It integrates seamlessly with Blazor WebAssembly, allowing developers to use a single, shared communication contract across their entire system architecture.

Explore detailed examples of using WitRPC with Blazor WebAssembly on GitHub: Blazor Client Example