WitCom and Blazor WebAssembly

Blazor WebAssembly is a powerful way to build web frontends (and beyond when hosted in MAUI, WinUI, or WPF) that share a unified C# object model and logic. Combined with a UI framework like MudBlazor, you can create web applications with the same ease as desktop applications—without worrying (much) about HTML, CSS, JavaScript, or other web-related technologies.

However, working with Blazor WebAssembly comes with some unique considerations.

Encryption in Blazor

Blazor WebAssembly runs in a browser environment and does not support System.Security.Cryptography. Therefore, you have two options when using WitCom:

Option 1: Disable Built-in Encryption

On the client side:

				
					Client = WitComClientBuilder.Build(options =>
{
    options.WithWebSocket($"ws://localhost:{PORT}/webSocket/");
    options.WithoutEncryptor();
    options.WithJson();
    options.WithLogger(Logger!);
    options.WithTimeout(TimeSpan.FromSeconds(1));
});

				
			

On the server side:

				
					Server = WitComServerBuilder.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

WitCom allows you to define a custom encryption mechanism compatible with Blazor WebAssembly.

In the example below, encryption is implemented using the Web Crypto API, invoked via JavaScript Interop. This means part of the logic is offloaded to a JavaScript component (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 EncryptorClientWeb class in C# uses IJSRuntime to invoke the JavaScript methods:

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

Register it in the service collection:

				
					builder.Services.AddScoped<EncryptorClientWeb>();
				
			

Inject it into components or services:

				
					[Inject]
public EncryptorClientWeb EncryptorClientWeb { get; private set; }
				
			

Pass it to WitCom when building the client:

				
					Client = WitComClientBuilder.Build(options =>
{
    options.WithWebSocket($"ws://localhost:{PORT}/webSocket/");
    options.WithEncryptor(EncryptorClientWeb);
    options.WithJson();
    options.WithLogger(Logger!);
    options.WithTimeout(TimeSpan.FromSeconds(1));
});

				
			

Blazor WebAssembly and AoT

By default, Blazor WebAssembly uses JIT compilation, which allows for dynamic features such as DynamicProxy. However, you can enable AoT (ahead-of-time) compilation for better performance:

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

				
			

With AoT enabled, dynamic proxies cannot be used. Instead, you must switch to a static proxy:

				
					Service = Client.GetService<IExampleService>(x => new ExampleServiceProxy(x), false);
				
			

You can find more details about static and dynamic proxies in this article.

References and Resources

To explore more about WitCom, refer to the following resources:

Conclusion

WitCom is not limited to .NET services or desktop applications. It is also an excellent choice for building web UIs with Blazor WebAssembly, enabling you to use a single shared contract across all system components.

Leave a Reply

Your email address will not be published. Required fields are marked *