A secure, zero-storage, zero-knowledge encrypted storage solution for Blazor applications.
- Blazor.ZeroStorage
- Table of Contents
- Features
- Installation
- API Reference
- Getting Started
- Advanced Application
- Zero-Knowledge: The library cannot read, decrypt, or interpret any stored data without a user actively providing a hardware-derived key.
- Zero-Storage (Zero-Artifact): No authentication metadata, password hashes, manifests, or key envelopes are stored in the database.
- Stateless: The library holds no persistent state. Once ZeroStorage is locked or the browser tab closes, the key is wiped from memory.
- Authenticated Encryption: Utilizes AES-GCM with 256-bit keys, guaranteeing both data confidentiality and integrity.
- Cryptographic Blinding: Database lookup keys are deterministically hashed via Hash-based Message Authentication Code (HMAC), making it impossible for an attacker to read logical keys or infer relationships between records.
- Cryptographic Sandboxing (Multi-User): Supports multiple users on the same device. Lookup keys are derived from the user's specific key, so a user's data exists in a mathematically isolated, invisible partition.
- Hardware-Bound Entropy: Utilizes the WebAuthn Pseudo-Random Function (PRF) extension to extract cryptographically secure 256-bit encryption keys directly from the user's hardware passkey.
- Metadata Obfuscation: Supports uniform cryptographic padding to mask the true byte-size of payloads, preventing attackers from guessing content based on payload length.
- Per-Item Key Derivation: Uses HMAC-based Extract-and-Expand Key Derivation Function (HKDF) to generate a unique encryption key for every single record, isolating any potential compromise to a single item.
- Same-Origin Isolation: Built on top of the browser's native IndexedDB, guaranteeing that the encrypted data is inaccessible to other domains or websites.
- Stateless Recovery: Allows exporting the raw encryption key as a typo-resistant Base58 string. This enables complete recovery without storing any fallback mechanisms or backup hashes in the database.
To install the NuGet package, run the following command:
dotnet add package Blazor.ZeroStorageTip
See here for more information on installing NuGet packages.
| Name | Description |
|---|---|
| ZeroStorage | A secure, zero-storage, zero-knowledge encrypted storage solution for Blazor applications. |
| Name | Description |
|---|---|
| IServiceCollection | Extension methods to add ZeroStorage services. |
| Convert | Extension methods to add conversion functionality. |
To add ZeroStorage services to your Blazor application, call the AddZeroStorage extension method on your IServiceCollection in the Program.cs file:
using Blazor.ZeroStorage;
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
// Add ZeroStorage services
builder.Services.AddZeroStorage();
await builder.Build().RunAsync();
}
}To use ZeroStorage, inject the ZeroStorage service into your component or service:
@page "/"
@using Blazor.ZeroStorage
@inject ZeroStorage Storage-or-
using Blazor.ZeroStorage;
using Microsoft.AspNetCore.Components;
public partial class Home
{
[Inject]
public ZeroStorage Storage { get; set; } = null!;
}-or-
using Blazor.ZeroStorage;
public class MyService(ZeroStorage storage) {}Tip
See the Example Project for a more detailed demonstration of how to use the library in a Blazor application.
Because Blazor.ZeroStorage is a zero-storage, zero-knowledge library, it provides a very minimal API surface.
This means that while it can be used directly to store and retrieve encrypted records, additional design patterns and abstractions may be desired to build a full-featured application on top of it.
Note
The following examples are heavily simplified for demonstration purposes. In a production application, you would likely want to add error handling, logging, etc.
One possible abstraction is to create a "Record Manager" wrapper around ZeroStorage.
This Record Manager maintains an internal list of keys to allow for features like listing all keys or clearing a user's data, which are not natively supported by ZeroStorage due to its zero-knowledge design.
using Blazor.ZeroStorage;
public class RecordManager(ZeroStorage storage)
{
// A internal key to track all keys that have been set, so we can implement GetKeys and Clear
private const string Keys = "__keys";
public async Task SetAsync(string key, string value)
{
// Get the current list of keys, add the new key if it doesn't exist, and save it back to storage
var keys = await storage.GetAsync<ICollection<string>>(Keys) ?? [];
if (!keys.Contains(key))
{
keys.Add(key);
await storage.SetAsync(Keys, keys);
}
await storage.SetAsync(key, value);
}
public async Task<string?> GetAsync(string key)
{
return await storage.GetAsync<string>(key);
}
public async Task DeleteAsync(string key)
{
// Get the current list of keys, remove the key if it exists, and save it back to storage
var keys = await storage.GetAsync<ICollection<string>>(Keys) ?? [];
if (keys.Remove(key))
await storage.SetAsync(Keys, keys);
await storage.DeleteAsync(key);
}
public async Task<ICollection<string>> GetKeysAsync()
{
return await storage.GetAsync<ICollection<string>>(Keys) ?? [];
}
public async Task ClearAsync()
{
// Get all keys and delete them one by one, then delete the internal key list as well
var keys = await GetKeysAsync();
foreach (var key in keys)
await storage.DeleteAsync(key);
await storage.DeleteAsync(Keys);
}
}Another possible abstraction is to create a TTL (Time-To-Live) wrapper around ZeroStorage to allow for expiring records after a certain amount of time.
This can be implemented by storing a timestamp along with the value and checking it on retrieval.
using Blazor.ZeroStorage;
internal record TtlRecord<T>(T Value, DateTime Expiry);
public class TtlStorage(ZeroStorage storage)
{
public async Task SetAsync<T>(string key, T value, TimeSpan ttl)
{
// Create a TtlRecord with the value and the expiry time, then save it to storage
var record = new TtlRecord<T>(value, DateTime.UtcNow.Add(ttl));
await storage.SetAsync(key, record);
}
public async Task<T?> GetAsync<T>(string key)
{
// Retrieve the TtlRecord from storage, check if it has expired, and return the value if it is still valid
var record = await storage.GetAsync<TtlRecord<T>>(key);
if (record is null) return default;
// If the record has expired, delete it from storage and return default
if (DateTime.UtcNow > record.Expiry)
{
await storage.DeleteAsync(key);
return default;
}
return record.Value;
}
}