IronWeave.Blobs
0.3.0
dotnet add package IronWeave.Blobs --version 0.3.0
NuGet\Install-Package IronWeave.Blobs -Version 0.3.0
<PackageReference Include="IronWeave.Blobs" Version="0.3.0" />
<PackageVersion Include="IronWeave.Blobs" Version="0.3.0" />
<PackageReference Include="IronWeave.Blobs" />
paket add IronWeave.Blobs --version 0.3.0
#r "nuget: IronWeave.Blobs, 0.3.0"
#:package IronWeave.Blobs@0.3.0
#addin nuget:?package=IronWeave.Blobs&version=0.3.0
#tool nuget:?package=IronWeave.Blobs&version=0.3.0
IronWeave.Blobs — C# Binding
A .NET wrapper for the iwblobs native library, providing chunked AES-256-GCM streaming encryption with per-recipient ECDH key envelopes and protobuf-based signed blob envelopes. Encryption and decryption are exposed as standard System.IO.Stream subclasses — IwBlobEncryptStream (write-only) and IwBlobDecryptStream (read-only) — while utilities and envelope validation are static methods on IronWeave.Blobs.IwBlobs.
Installation
Install the NuGet package (includes native binaries for all supported platforms):
dotnet add package IronWeave.Blobs
Requirements
- .NET 10
- Native
iwblobslibrary (built from the Rust crate withcargo build --release) - Protocol Buffers compiler (
protoc) — only needed for projects using the generatedBlobEnvelopeproto types
Project Structure
bindings/csharp/
IronWeave.Blobs.csproj Class library
IwBlobs.cs Static API + IwBlobEncryptStream / IwBlobDecryptStream
LICENSE Proprietary license (packed into the NuGet package)
README.md This file (packed into the NuGet package)
tests/
IronWeave.Blobs.Tests.csproj xUnit test project
IwBlobsTests.cs FFI wrapper round-trip tests
Adding to Your Project
Reference the class library project:
<ProjectReference Include="path/to/bindings/csharp/IronWeave.Blobs.csproj" />
Your project must enable unsafe blocks since the wrapper uses pointer-based P/Invoke internally:
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
The native library must be resolvable at runtime. For development, register a NativeLibrary.SetDllImportResolver to point at the Cargo build output:
using System.Runtime.InteropServices;
NativeLibrary.SetDllImportResolver(
typeof(IwBlobs).Assembly,
(libraryName, assembly, searchPath) =>
{
if (libraryName == "iwblobs")
{
// Adjust path to your cargo build output
return NativeLibrary.Load("/path/to/target/release/libiwblobs.dylib");
}
return IntPtr.Zero;
});
For production, place the native library where .NET can find it (application directory, system library path, or bundled in a NuGet runtime package).
.NET MAUI Deployment
The same iwblobs native binaries work across all MAUI target platforms — no special MAUI-specific builds are needed. The differences are purely in packaging and linking.
iOS
MAUI uses static linking on iOS. Reference the .a file and use [LibraryImport("__Internal")] instead of [LibraryImport("iwblobs")]:
<ItemGroup Condition="$(TargetFramework.Contains('ios'))">
<NativeReference Include="path/to/libiwblobs.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
</NativeReference>
</ItemGroup>
The IwBlobs wrapper uses [LibraryImport("iwblobs")] by default. For iOS static linking, route to __Internal via a conditional DllImportResolver or a build-time substitution.
Android
MAUI uses the same .so files produced by the Android NDK (via cargo-ndk). Place them under the NuGet RID directories:
| NDK ABI | NuGet RID |
|---|---|
arm64-v8a/libiwblobs.so |
runtimes/android-arm64/native/ |
armeabi-v7a/libiwblobs.so |
runtimes/android-arm/native/ |
x86_64/libiwblobs.so |
runtimes/android-x64/native/ |
Alternatively, add the .so files as AndroidNativeLibrary items:
<ItemGroup Condition="$(TargetFramework.Contains('android'))">
<AndroidNativeLibrary Include="libs/arm64-v8a/libiwblobs.so" Abi="arm64-v8a" />
<AndroidNativeLibrary Include="libs/armeabi-v7a/libiwblobs.so" Abi="armeabi-v7a" />
<AndroidNativeLibrary Include="libs/x86_64/libiwblobs.so" Abi="x86_64" />
</ItemGroup>
Desktop (macOS / Windows / Linux)
Standard dynamic linking via [LibraryImport("iwblobs")]. Place the native library (.dylib / .dll / .so) in a NuGet runtimes/{rid}/native/ directory or alongside the application binary.
CI-Built Binaries
The release workflow stages a pre-organized runtimes/ directory by RID. The .csproj packs everything under <repo>/dist/nuget/runtimes/ into the NuGet package, so a single dotnet add package IronWeave.Blobs brings all platforms at once.
API Reference
All utilities and validation are static on IronWeave.Blobs.IwBlobs. Methods that can fail throw IwBlobsException (extends InvalidOperationException). The streaming classes are Stream subclasses and integrate with any Stream-based pipeline.
Static Utilities
using IronWeave.Blobs;
// Cryptographically secure random AES-256 key (32 bytes)
byte[] aesKey = IwBlobs.GenerateAesKey();
// 16-byte UUIDv7 (big-endian, time-ordered) — e.g. for the envelope UUID
byte[] uuid = IwBlobs.GenerateUuidV7();
// Single-shot SHA-256 (32-byte digest) — native implementation
byte[] digest = IwBlobs.Sha256(encryptedBytes);
Two constants are exposed: IwBlobs.DefaultChunkSize (1 MB) and IwBlobs.StreamHeaderSize (16 bytes).
Encrypting a Blob
IwBlobEncryptStream is a write-only stream. It writes the 16-byte stream header to the destination on construction, then emits one encrypted chunk per DefaultChunkSize (or the chunkSize you pass, 1 byte–16 MB). Disposing flushes the final chunk and finalizes the GCM state.
byte[] aesKey = IwBlobs.GenerateAesKey();
await using var output = File.Create("encrypted.bin");
await using (var enc = new IwBlobEncryptStream(output, aesKey))
{
await using var input = File.OpenRead("input.bin");
await input.CopyToAsync(enc);
} // dispose here finalizes encryption and flushes the last chunk
// Custom chunk size (e.g. 4 MB):
await using var enc2 = new IwBlobEncryptStream(output, aesKey, chunkSize: 4 * 1024 * 1024);
The 16-byte header is also available via the Header property (a defensive copy) if you need to store it out-of-band rather than inline at the front of the ciphertext stream.
Note:
Flush()is intentionally a no-op — partial-chunk encryption is meaningless under the chunked GCM scheme. Chunks are emitted only when full or on dispose.
Decrypting a Blob
IwBlobDecryptStream is a read-only stream. It reads the 16-byte header from the source on construction, then decrypts and verifies one chunk at a time. Disposing verifies the final chunk was seen, detecting truncation.
await using var input = File.OpenRead("encrypted.bin");
await using var dec = new IwBlobDecryptStream(input, aesKey);
await using var output = File.Create("output.bin");
await dec.CopyToAsync(output);
A wrong key, reordered/dropped chunks, or a truncated stream all surface as an IwBlobsException (GCM authentication failure or unexpected end-of-stream).
SHA-256 Hashing
For the encrypted_hash field in a BlobEnvelope, you can use the native hash or — preferably — the .NET built-ins:
using System.Security.Cryptography;
// Recommended: .NET built-in
byte[] hash = SHA256.HashData(encryptedBytes);
// Or streaming over chunks:
using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
hasher.AppendData(chunk1);
hasher.AppendData(chunk2);
byte[] streamingHash = hasher.GetHashAndReset();
// Or the native FFI implementation (byte-identical):
byte[] nativeHash = IwBlobs.Sha256(encryptedBytes);
The algorithm is SHA-256 in all cases; the envelope's Hash proto carries HASH_TYPE_SHA2_256 to make the wire representation self-describing (see Working with Proto Types).
Envelope Validation
// Structural validation only — for envelope authors, pre-sign
IwBlobs.ValidateBlobEnvelope(envelopeBytes);
// Ed25519 signature + structural validation — for verifiers
IwBlobs.ValidateSignedBlobEnvelope(signedEnvelopeBytes);
Both methods throw IwBlobsException on failure and return normally on success. Structural validation enforces, among other things, that every Address carries a known AddressType of the correct length and that encrypted_hash is exactly HASH_TYPE_SHA2_256 + 32 bytes. The signed validator additionally checks the Ed25519 signature and the (AddressType, SignatureType) allowlist, failing closed on unknown pairs.
Working with Proto Types
BlobEnvelope / SignedBlobEnvelope are protobuf messages. Add proto codegen to your project to construct and parse them:
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="all" />
<Protobuf Include="path/to/proto/basic_types.proto" ProtoRoot="path/to/proto" GrpcServices="None" />
<Protobuf Include="path/to/proto/blob_envelope.proto" ProtoRoot="path/to/proto" GrpcServices="None" />
blob_envelope.proto imports basic_types.proto — both must be compiled with the same ProtoRoot. Generated C# types land in the Weave.Blobs namespace (envelope types) and Weave.Common namespace (shared Address, UUID, SignatureType, Hash, HashType).
Key points when assembling an envelope by hand:
using Google.Protobuf;
using Weave.Common;
using Weave.Blobs;
// Every Address carries an AddressType discriminator (Ed25519 = 0 today).
var creator = new Address
{
AddressType = AddressType.Ed25519,
Value = ByteString.CopyFrom(creatorPublicKey), // 32 bytes for Ed25519
};
// encrypted_hash is a typed Hash, NOT bare bytes.
// You MUST set hash_type explicitly: the proto3 default 0 = HASH_TYPE_SHA3_256
// (the chain's hash), which iwblobs does not produce and which validation rejects.
var attachment = new BlobAttachment
{
EncryptedHash = new Hash
{
HashType = HashType.Sha2256,
Value = ByteString.CopyFrom(SHA256.HashData(encryptedBytes)),
},
// ... AES-encrypted key shares, storage locations, etc.
};
An envelope can be standalone (SignedBlobEnvelope) or attached to a CryptoOperation from IronWeave.Crypto by referencing the envelope UUID in the operation's context.
Chunked Wire Format
| Region | Layout |
|---|---|
| Stream header (16 bytes) | 12-byte random base nonce + 4-byte LE u32 version (currently 1) |
| Per chunk | 4-byte LE u32 payload length + ciphertext + 16-byte GCM tag |
Each chunk's nonce is base_nonce XOR chunk_index; a 33-byte AAD binds a domain separator, the chunk index, an is_last_chunk flag, and the base nonce — defending against truncation, reordering, and cross-blob chunk swaps. The stream classes handle all of this internally; you only ever see plaintext on one side and the opaque ciphertext stream on the other.
Error Handling
| API | Failure behavior |
|---|---|
GenerateAesKey, GenerateUuidV7, Sha256 |
Throws IwBlobsException |
ValidateBlobEnvelope, ValidateSignedBlobEnvelope |
Throws IwBlobsException (returns normally on success) |
IwBlobEncryptStream / IwBlobDecryptStream ctor & I/O |
Throws IwBlobsException (FFI errors), ArgumentException (bad key length), ArgumentOutOfRangeException (bad chunk size) |
IwBlobsException exposes structured fields for programmatic handling:
| Property | Type | Description |
|---|---|---|
Function |
string |
The FFI function that failed (e.g. "iwblob_decrypt_stream_write") |
Code |
int |
Return code: 1 = input/format, 2 = internal panic, 3 = verification failed, 6 = signature invalid, 7 = encryption/decryption failed |
Detail |
string? |
Human-readable detail from iwblob_last_error() |
Running Tests
Build the native library first, then run the C# tests:
# Build the native library
cargo build --release
# Run all C# tests (10 tests)
cd iwblobs/bindings/csharp/tests && dotnet test
IwBlobsTests covers FFI wrapper round-trips: AES key / UUID generation, single-shot and streaming SHA-256, full chunked encrypt → decrypt cycles (including empty and multi-chunk payloads), and envelope structural/signature validation.
License
Proprietary. All rights reserved. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Google.Protobuf (>= 3.35.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.