Client-side payload encryption
RespireDataProtectionValueCodec in Respire.DataProtection implements IRespireValueCodec
using ASP.NET Core IDataProtector. It authenticates and encrypts bytes before sending them
to Redis. Enable it independently for each feature through existing codec or serializer hooks.
Without it, existing code paths, allocations, and stored formats are unchanged.
Install Respire.DataProtection. Create a protector from your configured
IDataProtectionProvider, using a stable purpose for each feature and tenant boundary.
Readers and writers need the same application name, purpose, key ring, and compression mode.
using Microsoft.AspNetCore.DataProtection;
using Respire.Compression;
using Respire.DataProtection;
static RespireDataProtectionValueCodec CreatePayloadCodec(IDataProtectionProvider provider)
=> new(provider.CreateProtector("my-app:cache:tenant-42:v1"),
new RespireBrotliValueCodec(new RespireValueCodecOptions
{
MaximumDecodedLength = 1024 * 1024,
}));
The optional second argument compresses before encryption. Reads authenticate and decrypt before decompressing. Omit it for encryption without compression. The codec adds versioned purposes, including the compression mode, so changing between compressed and raw mode fails authentication. Changing algorithms follows the codec migration rules. Compression bounds do not bound ciphertext buffering or Data Protection's decryption allocation. Limit input sizes at the application boundary.
Distributed cache and HybridCache
Set RespireCacheOptions.ValueCodec. This encrypts the cache's data field, including raw
bytes and IBufferDistributedCache operations. The client serializer does not control this
field. HybridCache L2 uses this setting on its Respire distributed-cache backend; HybridCache
L1 still holds ordinary application values in process memory.
using Microsoft.AspNetCore.DataProtection;
using Respire;
using Respire.Caching;
using Respire.DataProtection;
static RespireDistributedCache CreateEncryptedCache(IRespireClient client, IDataProtectionProvider provider)
=> new(client, new RespireCacheOptions
{
InstanceName = "encrypted:",
ValueCodec = new RespireDataProtectionValueCodec(provider.CreateProtector("app:cache:v1")),
});
Dispose the returned cache when finished. See cache registration for DI and HybridCache configuration.
Near-cache maps
Wrap the map's value delegates. These run for every stored map value, including primitives. The server-assisted near cache retains ciphertext; each read decrypts it. Loaders, persistence callbacks, and returned objects see plaintext.
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Respire;
using Respire.DataProtection;
static RespireMap<string, string> CreateEncryptedMap(IRespireClient client, IDataProtectionProvider provider)
{
var codec = new RespireDataProtectionValueCodec(provider.CreateProtector("app:map:v1"));
return new RespireMap<string, string>(client, "private-map", key => key,
value => codec.Encode(Encoding.UTF8.GetBytes(value)),
bytes => Encoding.UTF8.GetString(codec.Decode(bytes.Span)));
}
Enable ClientSideCache on the client and dispose the map before the client.
See near-cache maps for loading and persistence.
Job arguments and stream worker payloads
Compose encryption with the producer and consumer delegates. For objects, replace UTF-8 conversion with your serializer, such as source-generated JSON. Job queues use the same producer delegate for immediate, scheduled, recurring, and flow arguments. Retries and dead-letter copies retain encoded arguments.
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Respire.DataProtection;
using Respire.Streaming;
static void AddEncryptedJobs<THandler>(IServiceCollection services, IDataProtectionProvider provider)
where THandler : class, IRespireStreamHandler<RespireJob<string>>
{
var codec = new RespireDataProtectionValueCodec(provider.CreateProtector("app:jobs:v1"));
services.AddRespireJobQueue<THandler, string>("private-jobs",
value => codec.Encode(Encoding.UTF8.GetBytes(value)),
bytes => Encoding.UTF8.GetString(codec.Decode(bytes)));
}
For ordinary streams, encrypt each sensitive field on the producer and decode the same field
in the worker. Register IRespireClient separately as described in stream workers.
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Respire;
using Respire.DataProtection;
using Respire.Streaming;
static async Task ConfigureEncryptedStream<THandler>(IServiceCollection services,
IRespireClient client, IDataProtectionProvider provider, string message)
where THandler : class, IRespireStreamHandler<string>
{
var codec = new RespireDataProtectionValueCodec(provider.CreateProtector("app:stream:v1"));
services.AddRespireStreamWorker<THandler, string>("private-events", "readers",
entry => Encoding.UTF8.GetString(codec.Decode(entry["payload"])));
await client.Streams.AddAsync("private-events",
("payload", codec.Encode(Encoding.UTF8.GetBytes(message))));
}
Typed client serialization
Set RespireOptions.Serializer to new RespireValueCodecSerializer(yourSerializer, encryptionCodec)
to encrypt serializer-backed values. Primitive and raw paths (strings, numbers, byte arrays,
and RespireValue) bypass the serializer. For these, call Encode and Decode explicitly
or use the feature delegates above. See where codecs apply.
Authentication failures and migration
Tampered, truncated, plaintext, wrong-purpose, and unavailable-key payloads throw
CryptographicException with a payload-authentication message and the original exception.
Decompression and deserialization never run on unauthenticated bytes. Destination overloads
append nothing until the complete decode succeeds. Invalid authenticated compression
frames can instead throw InvalidDataException.
There is no plaintext fallback. Use a new key namespace or migrate old values with the old reader and new writer. All producers and consumers must agree on configuration. Encryption is randomized: ciphertext equality cannot support set membership, compare-and-swap, or deduplication. Job IDs continue to control job deduplication.
Key rotation and storage
Data Protection writes using its current active key and reads with retained expired keys. Rotation does not rewrite existing payloads. Retain keys for the lifetime of cached values, queued and scheduled jobs, retries, and dead letters. Deleted or revoked keys cannot decrypt their payloads; changing application names or purposes also prevents reads. Key-ring changes take effect as Data Protection refreshes each process's cached ring; revocation is not an instantaneous distributed kill switch. See Microsoft's key management documentation.
Persist and share the key ring across instances and restarts; do not use an ephemeral
provider for durable data. If you store keys in Redis,
protect key material separately, for example with ProtectKeysWithCertificate, and keep
the private certificate outside Redis. Unencrypted keys beside ciphertext do not protect
against someone who can read the whole Redis database.
What remains visible
Redis keys, hash and stream field names, TTLs, lengths, access patterns, job IDs, schedules, groups, tracing fields, progress/results, and other metadata remain visible. Only bytes passed through the codec are encrypted. Protect job completion results and progress messages separately if they contain secrets. Search indexes, JSON fields, and vector attributes remain plaintext unless separately transformed; server-side search and computation cannot operate on encrypted contents. Ciphertext length can reveal compression ratios; omit compression when that leakage is unsuitable for your payloads.
Handlers receive plaintext, and this codec does not protect application memory. Use TLS and avoid logging sensitive values. Valid ciphertext can be replayed or moved between keys sharing a purpose: the codec does not authenticate the Redis key or provide freshness.