NostrSharp 1.0.1

dotnet add package NostrSharp --version 1.0.1                
NuGet\Install-Package NostrSharp -Version 1.0.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NostrSharp" Version="1.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add NostrSharp --version 1.0.1                
#r "nuget: NostrSharp, 1.0.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install NostrSharp as a Cake Addin
#addin nuget:?package=NostrSharp&version=1.0.1

// Install NostrSharp as a Cake Tool
#tool nuget:?package=NostrSharp&version=1.0.1                

NostrSharp

A library that provide a set of tools for Nostr developement

Preview

This library is inspired by this repo. You can freely use NostrSharp in any kind of projects you want.

IMPORTANT! This library is not yet tested enough for a production environment. Feel free to try it, send feedback and contribute.

Download the package from Nuget or search it by typing NostrSharp.

This library is meant to provide necessary tools to build your own nostr-based software It's usable both on desktop/mobile and web environments.

I've implemented all kinds of events detailed in the Nostr repository, HOWEVER i didn't yet tested most of the implementations i made, so be careful.

Some of the things i tested:

  • multiple relays connection
  • obtaining relays NIP-11 information document
  • requesting events with filters
  • NPub and NSec generation
  • keys conversions between hex and bech32
  • events signing
  • events serialization/deserialization
  • publishing events to multiple relays

How to use it

The easiest way in my opinion is to rely entirely on the NSMain class, however, since the vast majority of the library's classes and methods are public you are not forced to.

NSMain ns = new NSMain();

// Initialize it by giving at least one of NPub or NSec
ns.Init(nPub, nSec);

// Connect to a list of relays by they uris
await ns.ConnectRelays(relaysUriList);
// Add a new relay in any moment
ns.AddRelay(relayUri);
// Remove a relay in any moment
await ns.DisconnectRelay(relayUri);

// Asking to all connected relays all my Kind 1 events (uses previously given NPub)
await ns.GetMyShortTextNotes();

// Don't forget to dispose
ns.Dispose();

When you want to create and fill a new event you could use the static class NSEventMaker. This class contains a comprehensive collection of helper methods to help you generate the NEvent you want.

For example, to create a ready-to-sign event for a short text note you could write the following:

NEvent ev = NSEventMaker.ShortTextNote("Hello World! This is my first note!");

The resulting NEvent is the instance you need to sign and then broadcast to relays:

// If the signing procedure goes well
if (ev.Sign(nSec))
{
    // I can broadcast the event to all my connected relays
    await ns.SendEvent(ev);

    // Or maybe to just one
    await ns.SendEvent(specificRelayUri, ev);
}

Whenever i'm requesting a filtered set of events, or broadcasting a new one, i will always need to listen for result events:

// If i want to listen for all Kind 1 events i receive from relays i have to subscribe to this event
ns.OnShortTextNote += OnShortTextNote;

void OnShortTextNote(Uri relayUri, NEvent receivedShortTextNoteEvent)
{
    // I receive the uri of the relay that has sent the event, and the deserialized event itself
}

// If i want to know if an event i broadcasted is approved or refused
ns.OnEventApproved += OnEventApproved;
ns.OnEventRefused += OnEventRefused;

void OnEventApproved(Uri relayUri, string eventId, string message)
{
    // I receive the uri of the responding relay, the id of the event i tried to broadcast
    // and an optional error message from the relay describing the operation
}
void OnEventRefused(Uri relayUri, string eventId, string message)
{
    // I receive the uri of the responding relay, the id of the event i tried to broadcast
    // and an optional text message from the relay describing the operation
}

Other Stuff

Keys

You can generate a new key pair

NSKeyPair kp = NSKeyPair.GenerateNew();

You can import an existing key from both Hex or Bech32 formats, and then have it on the other format

NSec nsec1 = NSec.FromBech32("bech32_nsec_string");
string hex = nsec1.Hex;

NSec nsec2 = NSec.FromHex("hex_nsec_string");
string bech32 = nsec2.Bech32;

Utilities

You can verify NIP05:

NIP05? checkedNIP05 = await NSUtilities.CheckNIP05(nPubHexToCheck, nip05String);
if (checkedNIP05 is not null)
{
    // Valid NIP-05
}

When you need to parse the event content to get all embedded references to events or profiles, you can use EventContentParser

// This return a list of all the identifiers found inside the event's content
List<NostrIdentifier> identifiers = EventContentParser.ParseEventContent(eventToBeParsed);

WEB ENVIRONMENT

Because of .NET 6 current lack of support for all encryption primitives, i came up with the possibility to pass in custom encryption/decryption methods to extend this support in framework like Blazor.

For example if you check the EncryptBase64 method inside NEvent class, you can see how it uses System.Security.Cryptography.Aes: this has no support at all on .NET 6 and a very limited support on .NET 7, so this will throw PlatformNotSupported exceptions respectively on 'Aes.Create()' for .net 6 and 'aes.EncryptCbc' on .net 7, making impossible to use NIP-04 on Blazor

// Not supported on .NET 6
using Aes aes = Aes.Create();
// Not supported on .NET 6 and .NET 7
byte[] cbcEncrypted = aes.EncryptCbc(plainText, aes.IV);

HOWEVER HERE'S THE SOLUTION I FOUND

Add this on you Blazor's project wwwroot/index.html file somewhere inside the body tag

<script>
    window.ArrayBufferToBase64 = function (arrayBufferText) {
        var binary = '';
        var bytes = new Uint8Array(arrayBufferText);
        var len = bytes.byteLength;
        for (var i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return window.btoa(binary);
    };
    window.ImportAesCbcKey = async function (sharedKeyAsByteArray) {
        var cryptoKey = await crypto.subtle.importKey(
            "raw", // format
            sharedKeyAsByteArray, // keyData
            { name: "AES-CBC" }, // algorithm
            true, // extractable
            ["encrypt", "decrypt"], // keyUsages
        );
        console.log('cryptoKey: ' + cryptoKey);
        return cryptoKey;
    };


    /// <summary>
    /// Takes the key and the plain text to encrypt using AES-256-CBC algorithm.
    /// </summary>
    /// <param name="sharedKeyAsByteArray">Byte array representation of key's EC</param>
    /// <param name="plainTextAsByteArray">UTF8 plain text as byte array</param>
    /// <returns>The encrypted string formatted as => encrypted_text + '?iv=' + init_vector</returns>
    window.AesCbcEncrypt = async function (sharedKeyAsByteArray, plainTextAsByteArray) {

        // Import the key as an AES-256-CBC key and generate a init vector
        var cryptoKey = await window.ImportAesCbcKey(sharedKeyAsByteArray);
        const iv = crypto.getRandomValues(new Uint8Array(16));

        console.log('iv: ' + iv);

        // Run the encryption
        // reference: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt
        var encryptedText = await crypto.subtle.encrypt(
            { name: "AES-CBC", iv },
            cryptoKey,
            plainTextAsByteArray
        );
        console.log('encryptedText: ' + encryptedText);

        var base64encodedText = window.ArrayBufferToBase64(encryptedText);
        var base64encodedIV = window.ArrayBufferToBase64(iv);

        console.log('RESULT: ' + base64encodedText + "?iv=" + base64encodedIV);

        // And finally the result returned is always formatted with iv separator ("?iv=")
        return base64encodedText + "?iv=" + base64encodedIV;
    };

    /// <summary>
    /// Takes the key, the init vector and the encrypted text decrypt using AES-256-CBC algorithm.
    /// </summary>
    /// <param name="sharedKeyAsByteArray">Byte array representation of key's EC</param>
    /// <param name="ivAsByteArray">Byte array representation init vector</param>
    /// <param name="encryptedTextAsByteArray">UTF8 encrypted text as byte array</param>
    /// <returns>The encrypted string formatted as => encrypted_text + '?iv=' + init_vector</returns>
    window.AesCbcDecrypt = async function (sharedKeyAsByteArray, ivAsByteArray, encryptedTextAsByteArray) {

        // Import the key as an AES-256-CBC
        var cryptoKey = await window.ImportAesCbcKey(sharedKeyAsByteArray);

        // Run the decryption
        // reference: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt
        var decryptedText = await crypto.subtle.decrypt(
            { name: "AES-CBC", iv: ivAsByteArray.buffer }, // Init vector must be passed from outside
            cryptoKey,
            encryptedTextAsByteArray
        );

        // Convert the decrypted text from ArrayBuffer to Uint8Array, and then decode it as string
        return new TextDecoder('utf-8').decode(new Uint8Array(decryptedText));
    };
</script>

Then create a new class with this code

public class BlazorAesCbc
{
    [Inject]
    private IJSRuntime JS { get; set; }

    public BlazorAesCbc(IJSRuntime js)
    {
        this.JS = js;
    }


    public async Task<string?> Encrypt(byte[] sharedKeyAsByteArray, byte[] plainTextAsByteArray)
    {
        try
        {
            string result = await JS.InvokeAsync<string>("AesCbcEncrypt", sharedKeyAsByteArray, plainTextAsByteArray);
            return result;
        }
        catch (Exception ex)
        {
            return null;
        }
    }
    public async Task<string?> Decrypt(byte[] sharedKeyAsByteArray, byte[] ivAsByteArray, byte[] encryptedTextAsByteArray)
    {
        try
        {
            string result = await JS.InvokeAsync<string>("AesCbcDecrypt", sharedKeyAsByteArray, ivAsByteArray, encryptedTextAsByteArray);
            return result;
        }
        catch (Exception ex)
        {
            return null;
        }
    }
}

And, for the last step, add this on you Program.cs file, right before calling var host = builder.Build();

builder.Services.AddSingleton<BlazorAesCbc>();

Now, whenever you need to encrypt/decrypt an event's Content, you can import the BlazorAesCbc class inside your page and pass his methods to encrypt/decrypt as shown here:

// This at the top of the page
@inject BlazorAesCbc bai

// This where you need to encrypt
NEvent? ev = await NSEventMaker.EncryptedDirectMessage("test di criptazione", ns.NPub, ns.NSec, bai.Encrypt);

// This where you need to decrypt
string? decryptedMsg = await ev.Decrypt(ns.NSec, bai.Decrypt);
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 188 9/25/2023
1.0.0 135 9/22/2023