WitRPC secures communication on two independent axes: encryption protects data in transit, and authorization controls who may connect. Both are enabled with builder options and require no key or certificate management for the common cases.

End-to-end encryption

Encryption is switched on with one call on each side:

csharp
// Server
var server = WitServerBuilder.Build(options =>
{
    options.WithService(new MyService());
    options.WithTcp(5000, maxNumberOfClients: 100);
    options.WithEncryption();
});
server.StartWaitingForConnection();

// Client
var client = WitClientBuilder.Build(options =>
{
    options.WithTcp("localhost", 5000);
    options.WithEncryption();
});
await client.ConnectAsync(TimeSpan.FromSeconds(5), CancellationToken.None);

During the handshake, the two sides perform an RSA key exchange to establish a shared AES-256 key; from then on every message (calls, responses, and events alike) is encrypted with it. Key generation and exchange are handled by the framework.

Encryption settings must match. If one side enables encryption and the other does not, the handshake fails and no connection is established. To run deliberately unencrypted, call WithoutEncryption() on both sides; this is reasonable for local IPC or when the transport layer already provides TLS.

Transport-level TLS

Message-layer encryption and transport TLS are independent tools, and either satisfies the "encrypt on untrusted networks" rule:

  • TCP with TLS: WithTcpSecure(port, maxNumberOfClients, certificate) on the server and WithTcpSecure(host, port, targetHost, sslValidationCallback) on the client, where targetHost matches the certificate name and the callback can be null for standard validation.
  • Secure WebSocket: an https:// listener with a certificate on the server, wss:// on the client.

TLS also gives non-WitRPC clients (browsers, REST callers) transport security. Message-layer encryption adds protection that survives TLS termination points such as reverse proxies. Using both together is valid when the threat model calls for it.

Token-based authorization

Access control uses tokens presented during the handshake:

csharp
// Server: require a token
var server = WitServerBuilder.Build(options =>
{
    options.WithService(new MyService());
    options.WithTcp(5000, maxNumberOfClients: 100);
    options.WithEncryption();
    options.WithAccessToken("Secr3tToken");
});

// Client: present the token
var client = WitClientBuilder.Build(options =>
{
    options.WithTcp("localhost", 5000);
    options.WithEncryption();
    options.WithAccessToken("Secr3tToken");
});

The client sends the token during connection; the server accepts the connection only if it matches. A client with a missing or wrong token is rejected before any method call can happen. If no authorization is needed, WithoutAuthorization() (or simply not configuring a token) leaves the server open.

Dynamic tokens and custom validation

A single static token is the simplest scheme; both sides can go further.

On the client, WithAccessToken has overloads that take a callback (Func<string> or Func<Task<string>>), and WithAccessTokenProvider accepts an IAccessTokenProvider implementation. Use these when tokens expire or come from an identity provider: the framework requests a fresh token when it needs one instead of caching a stale string.

On the server, WithAccessTokenValidator accepts an IAccessTokenValidator implementation with your own logic: look tokens up in a database, verify JWT signatures, enforce per-client keys or roles. Whatever the validator decides during the handshake determines whether the connection is accepted.

Encryption in Blazor WebAssembly: BouncyCastle

The standard encryption relies on .NET cryptography APIs that are not fully available inside the browser sandbox, so Blazor WebAssembly clients need the BouncyCastle-based alternative. BouncyCastle is a pure C# cryptography library that runs everywhere .NET runs, browser included; the security scheme stays the same (RSA-OAEP key exchange, AES-256-CBC data encryption).

Install OutWit.Communication.Client.Encryption.BouncyCastle and OutWit.Communication.Server.Encryption.BouncyCastle, then configure both sides with WithBouncyCastleEncryption() instead of WithEncryption():

csharp
// Server
var server = WitServerBuilder.Build(options =>
{
    options.WithWebSocket("http://localhost:5000", maxClients: 100);
    options.WithJson();
    options.WithBouncyCastleEncryption();
    options.WithService(new MyService());
});

// Client (Blazor WebAssembly)
var client = WitClientBuilder.Build(options =>
{
    options.WithWebSocket("ws://localhost:5000");
    options.WithJson();
    options.WithBouncyCastleEncryption();
});

The two encryption modes are not interoperable: a BouncyCastle server talks only to BouncyCastle clients and vice versa. Pick one mode per connection and configure it on both ends. For ordinary .NET-to-.NET communication the standard WithEncryption() is sufficient and avoids the extra dependency; reach for BouncyCastle when a WebAssembly (or otherwise restricted) client is involved. More on the WASM environment in Blazor WebAssembly & AOT →.

Practices worth following

Encrypt by default on any network. Enable encryption (or TLS) for every connection that leaves the machine. Skipping it is defensible only inside a controlled local environment, and the performance cost of AES is small enough that "just in case" is usually the right call.

Keep configurations symmetric. Every security option mirrors: encryption mode, token, validator expectations. A mismatch on any of them fails the handshake. Sourcing the shared values from one configuration point on both sides prevents drift.

Treat tokens like passwords. Keep them out of source code; supply them through configuration or environment variables, and rotate them on a schedule or on suspicion of exposure.

Grow into custom validation when one token stops being enough. Per-client keys, expiring tokens, and integration with an existing identity system all fit the IAccessTokenValidator / IAccessTokenProvider pair without changing anything else in the setup.

Test the failure paths. Verify that a client with a wrong token is rejected, and that mismatched encryption fails to connect. WithLogger(...) on the server shows exactly which handshake step refused a client, which turns security debugging from guesswork into reading a log line.