π Introduction to LibSodium.Net
LibSodium.Net provides .NET developers with easy-to-use bindings for libsodium, a powerful, modern cryptography library widely recognized for its simplicity and security. This makes it straightforward to add robust cryptographic functionality to your .NET applications.
β¨ Why LibSodium.Net?
- Cross-platform: Seamless support across Windows, Linux, and macOS.
- Modern Cryptography: Includes authenticated encryption, public-key cryptography, hashing, key derivation, and digital signatures.
- Simple and Secure API: Designed to reduce complexity, helping you implement cryptography correctly and securely.
- Secure Memory Handling: Sensitive data management to minimize risks like memory leaks or data exposure.
- Span
over Arrays : Optimized for performance and memory efficiency by usingSpan<T>
instead of heap-allocated arrays. - AOT Compatible: Uses
LibraryImport
(source-generated P/Invoke) instead ofDllImport
, making it fully compatible with AOT compilation environments.
π Getting Started with LibSodium.Net
Here's how you can quickly integrate LibSodium.Net into your .NET projects.
π¦ Install via NuGet
You can easily install LibSodium.Net using the NuGet package manager:
CLI
Using the .NET CLI:
dotnet add package LibSodium.Net
Visual Studio
- Right-click your project in Solution Explorer.
- Choose Manage NuGet Packages.
- Search for
LibSodium.Net
and click Install.
Package Manager Console
Install-Package LibSodium.Net
π§± SecretBox
The SecretBox
API in LibSodium.Net provides a simple and secure way to perform symmetric authenticated encryption using the XSalsa20 stream cipher and Poly1305 MAC. It supports both combined and detached encryption modes.
π SecretBox Features
- Symmetric authenticated encryption using XSalsa20-Poly1305
- Support for combined and detached modes
- Automatic or manual nonce handling
- Tamper-proof via MAC verification
π Encrypting and Decrypting Messages
β Combined Mode (Manual Nonce)
Span<byte> key = stackalloc byte[SecretBox.KeyLen];
Span<byte> nonce = stackalloc byte[SecretBox.NonceLen];
RandomGenerator.Fill(key);
RandomGenerator.Fill(nonce);
var plaintext = Encoding.UTF8.GetBytes("Hello, secure world!");
Span<byte> ciphertext = stackalloc byte[plaintext.Length + SecretBox.MacLen];
// Encrypt
var result = SecretBox.EncryptCombined(ciphertext, plaintext, key, nonce);
// Decrypt
Span<byte> decrypted = stackalloc byte[plaintext.Length];
var recovered = SecretBox.DecryptCombined(decrypted, result, key, nonce);
Console.WriteLine(Encoding.UTF8.GetString(recovered));
β Combined Mode (Auto Nonce)
Span<byte> key = stackalloc byte[SecretBox.KeyLen];
RandomGenerator.Fill(key);
var plaintext = Encoding.UTF8.GetBytes("Auto-nonce mode test");
Span<byte> ciphertext = stackalloc byte[plaintext.Length + SecretBox.MacLen + SecretBox.NonceLen];
var encrypted = SecretBox.EncryptCombined(ciphertext, plaintext, key);
Span<byte> decrypted = stackalloc byte[plaintext.Length];
var recovered = SecretBox.DecryptCombined(decrypted, encrypted, key);
Console.WriteLine(Encoding.UTF8.GetString(recovered));
π§© Detached Mode (Manual Nonce)
Span<byte> key = stackalloc byte[SecretBox.KeyLen];
Span<byte> nonce = stackalloc byte[SecretBox.NonceLen];
RandomGenerator.Fill(key);
RandomGenerator.Fill(nonce);
var plaintext = Encoding.UTF8.GetBytes("Detached mode message");
Span<byte> ciphertext = stackalloc byte[plaintext.Length];
Span<byte> mac = stackalloc byte[SecretBox.MacLen];
SecretBox.EncryptDetached(ciphertext, mac, plaintext, key, nonce);
Span<byte> decrypted = stackalloc byte[plaintext.Length];
var output = SecretBox.DecryptDetached(decrypted, ciphertext, key, mac, nonce);
Console.WriteLine(Encoding.UTF8.GetString(output));
π§© Detached Mode (Auto Nonce)
Span<byte> key = stackalloc byte[SecretBox.KeyLen];
RandomGenerator.Fill(key);
var plaintext = Encoding.UTF8.GetBytes("Auto-nonce detached mode");
Span<byte> ciphertext = stackalloc byte[plaintext.Length + SecretBox.NonceLen];
Span<byte> mac = stackalloc byte[SecretBox.MacLen];
SecretBox.EncryptDetached(ciphertext, mac, plaintext, key);
Span<byte> decrypted = stackalloc byte[plaintext.Length];
var output = SecretBox.DecryptDetached(decrypted, ciphertext, key, mac);
Console.WriteLine(Encoding.UTF8.GetString(output));
β οΈ Error Handling
SecretBox methods throw:
ArgumentException
β if inputs are the wrong size.LibSodiumException
β if decryption fails due to tampering or incorrect keys.
π Notes
- Always use a new random nonce for each encryption.
- Decryption verifies the MAC before returning plaintext.
- Use
RandomGenerator.Fill()
to securely fill nonces and keys.
SecretStream
The SecretStream
class in LibSodium.Net provides secure, authenticated stream-based encryption and decryption using the XChaCha20-Poly1305 algorithm. It's designed to handle large streams of data efficiently and securely.
Key Features
- Authenticated encryption ensures data integrity.
- Automatic chunking and handling of large data streams.
- Secure random key generation.
- Protection against nonce reuse.
Basic Usage
1. Generating a Secret Key
A secret key must be securely generated and managed:
byte[] key = new byte[CryptoSecretStream.KeyLen];
CryptoSecretStream.GenerateKey(key);
2. Encrypting Data
Encrypting data streams:
using var inputFile = File.OpenRead("plaintext.dat");
using var encryptedFile = File.Create("encrypted.dat");
await SecretStream.EncryptAsync(inputFile, encryptedFile, key);
Synchronous Encryption:
using var inputFile = File.OpenRead("plaintext.dat");
using var encryptedFile = File.Create("encrypted.dat");
SecretStream.Encrypt(inputFile, encryptedFile, key);
3. Decrypting Data
Decrypting the encrypted data back to plaintext:
using var encryptedFile = File.OpenRead("encrypted.dat");
using var decryptedFile = File.Create("decrypted.dat");
await SecretStream.DecryptAsync(encryptedFile, decryptedFile, key);
Synchronous Decryption:
using var encryptedFile = File.OpenRead("encrypted.dat");
using var decryptedFile = File.Create("decrypted.dat");
SecretStream.Decrypt(encryptedFile, decryptedFile, key);
Security Considerations
- Secure Key Management: Protect your keys; losing them or exposing them compromises security.
- Nonce Management: Handled internally by
SecretStream
; avoid manual nonce reuse. - Integrity Checks: Automatic using Poly1305 tags; any tampering results in exceptions.
Error Handling
Encryption and decryption throw specific exceptions for error conditions:
ArgumentException
: Invalid arguments (wrong key length, null streams).LibSodiumException
: Authentication failed, typically from tampered data.
Performance Considerations
SecretStream
processes data in chunks (default: 64KB) for optimal balance between memory usage and performance.- Utilize asynchronous methods (
EncryptAsync
/DecryptAsync
) for IO-bound scenarios for better scalability.
π‘οΈ SecureMemory
When working with cryptographic data, it's essential to ensure that sensitive information doesn't get leaked through memory dumps, swapping, or garbage collection. LibSodium.Net provides SecureMemory
and SecureMemory<T>
as robust tools to manage sensitive data safely in unmanaged memory.
These APIs leverage libsodium's sodium_malloc
, sodium_mlock
, and related memory protection functions to offer secure, optionally read-only memory regions that are wiped on disposal.
π§© SecureMemory<T>
SecureMemory<T>
is a managed wrapper around unmanaged memory that stores a span of unmanaged type T
. It ensures:
- Memory is allocated using
sodium_allocarray
. - Memory is wiped with
sodium_memzero
on disposal. - Optional read-only protection using
ProtectReadOnly()
. - Optional read-write toggle with
ProtectReadWrite()
. - Safe access through
Span<T>
,Memory<T>
,ReadOnlySpan<T>
, andReadOnlyMemory<T>
.
β Basic Usage
using var secure = SecureMemory.Create<byte>(32); // Allocate secure memory
var span = secure.AsSpan(); // Write access
RandomGenerator.Fill(span); // Fill with sensitive data
secure.ProtectReadOnly(); // Make it read-only
var readOnly = secure.AsReadOnlySpan(); // Safe read-only view
secure.ProtectReadWrite(); // Allow writing again
π Safety Features
- Accessing a
SecureMemory<T>
object after disposal throwsObjectDisposedException
. - Accessing writable span when read-only throws
InvalidOperationException
. - Any span or memory obtained prior to disposal becomes invalid. Using it after the object is disposed may result in
AccessViolationException
. - Writing through a span obtained before marking the memory as read-only will throw an
AccessViolationException
. - Memory is finalized and securely freed if not explicitly disposed.
π§ SecureMemory Utilities
SecureMemory
also provides utility methods for working with unmanaged memory spans directly:
π Allocate Secure Buffers
var span = SecureMemory.Allocate<byte>(64);
// Use span...
SecureMemory.Free(span); // Free when done
All allocations are initialized with
0xDB
for predictable testing.
π Zeroing Buffers
byte[] buffer = { 1, 2, 3 };
SecureMemory.MemZero(buffer); // Zeros array securely
π Lock/Unlock Memory
var span = SecureMemory.Allocate<byte>(128);
SecureMemory.MemLock(span); // Prevents swapping to disk
SecureMemory.MemUnlock(span);
SecureMemory.Free(span);
π Read-Only & Read-Write Protections
var span = SecureMemory.Allocate<long>(4);
var readOnlySpan = SecureMemory.ProtectReadOnly(span);
var writableSpan = SecureMemory.ProtectReadWrite(readOnlySpan);
β οΈ Security Considerations
- Do not pass managed memory (e.g.,
new byte[1024]
) toFree()
. Doing so can corrupt memory. - Secure memory functions work only with unmanaged memory allocated by
SecureMemory.Allocate()
. - Avoid using spans or memory references after the backing
SecureMemory<T>
has been disposed; doing so can cause undefined behavior orAccessViolationException
. - Writing to memory through a previously obtained span after calling
ProtectReadOnly()
will throw anAccessViolationException
. - Always dispose
SecureMemory<T>
when done, or useusing
to ensure cleanup.
β Unit-Tested Reliability
The implementation is backed by comprehensive tests ensuring:
- Memory is initialized to
0xDB
for testing predictability - Read-only enforcement and write protection
- Exception safety on misuse
- Proper finalization and disposal semantics
SecureMemory APIs are essential for handling secrets like keys, passwords, or tokens securely. They give you granular control over how and when memory is allocated, accessed, and clearedβadding another layer of defense in your cryptographic applications.