aboutsummaryrefslogtreecommitdiff
path: root/back-end
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-09-06 13:51:13 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-09-06 13:51:13 -0400
commitcd8e865dad326f85ff2357ad90bbd6aa65dea68e (patch)
tree0d4a0bb8bafc4f807407e99c5e6bf4e1cb34217a /back-end
initial commit
Diffstat (limited to 'back-end')
-rw-r--r--back-end/Taskfile.yaml48
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs175
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/IRandomSource.cs32
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs209
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj27
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs79
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs101
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/IClientAccessScope.cs48
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/IKvVaultStore.cs52
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/IVaultClientScope.cs33
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/IVaultKvClientScope.cs29
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/KvVaultStorage.cs66
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj27
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs156
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/VaultUserScope.cs25
-rw-r--r--back-end/plugins/nvault/src/Base64KeyEncoder.cs31
-rw-r--r--back-end/plugins/nvault/src/Endpoints/Endpoint.cs424
-rw-r--r--back-end/plugins/nvault/src/INostrCryptoProvider.cs66
-rw-r--r--back-end/plugins/nvault/src/INostrKeyEncoder.cs50
-rw-r--r--back-end/plugins/nvault/src/INostrOperations.cs35
-rw-r--r--back-end/plugins/nvault/src/INostrVault.cs33
-rw-r--r--back-end/plugins/nvault/src/ManagedCryptoprovider.cs81
-rw-r--r--back-end/plugins/nvault/src/ManagedVaultClient.cs71
-rw-r--r--back-end/plugins/nvault/src/Model/NostrContext.cs100
-rw-r--r--back-end/plugins/nvault/src/Model/NostrEvent.cs90
-rw-r--r--back-end/plugins/nvault/src/Model/NostrKeyMeta.cs89
-rw-r--r--back-end/plugins/nvault/src/Model/NostrKeyMetaStore.cs79
-rw-r--r--back-end/plugins/nvault/src/Model/NostrRelay.cs86
-rw-r--r--back-end/plugins/nvault/src/Model/NostrRelayFlags.cs24
-rw-r--r--back-end/plugins/nvault/src/Model/NostrRelayStore.cs78
-rw-r--r--back-end/plugins/nvault/src/NVault.csproj34
-rw-r--r--back-end/plugins/nvault/src/NativeSecp256k1Library.cs109
-rw-r--r--back-end/plugins/nvault/src/NostrEntry.cs55
-rw-r--r--back-end/plugins/nvault/src/NostrMessageKind.cs70
-rw-r--r--back-end/plugins/nvault/src/NostrOpProvider.cs282
35 files changed, 2994 insertions, 0 deletions
diff --git a/back-end/Taskfile.yaml b/back-end/Taskfile.yaml
new file mode 100644
index 0000000..6d36177
--- /dev/null
+++ b/back-end/Taskfile.yaml
@@ -0,0 +1,48 @@
+
+#taskfile for building the libraries for admin and clients and creating their packages
+
+version: '3'
+
+vars:
+ DOTNET_BUILD_FLAGS: '/p:RunAnalyzersDuringBuild=false /p:BuildInParallel=true /p:MultiProcessorCompilation=true'
+
+tasks:
+
+ build:
+ dir: '{{.USER_WORKING_DIR}}'
+ cmds:
+ #build project
+ - dotnet publish -c release {{.DOTNET_BUILD_FLAGS}}
+
+ #postbuild to package artifaces into the archives for upload
+ postbuild_success:
+ dir: '{{.USER_WORKING_DIR}}'
+ vars:
+ #output directory for the build artifacts
+ OUT_DIR: 'bin/release/{{.TARGET_FRAMEWORK}}/publish'
+
+ cmds:
+ #pack up source code
+ - task: packsource
+
+ #copy license to output dir
+ - powershell -Command "cp '{{.MODULE_DIR}}/LICENSE' -Destination '{{.OUT_DIR}}/LICENSE.txt'"
+
+ #tar the plugin output and put it in the bin dir
+ - cd {{.OUT_DIR}} && tar -czvf '{{.USER_WORKING_DIR}}/bin/release.tgz' .
+
+ packsource:
+ dir: '{{.USER_WORKING_DIR}}'
+ internal: true
+ cmds:
+ #copy source code to target
+ - powershell -Command "Get-ChildItem -Include *.cs,*.csproj -Recurse | Where { \$_.FullName -notlike '*\obj\*' -and \$_.FullName -notlike '*\bin\*' } | Resolve-Path -Relative | tar --files-from - -cvzf 'bin/src.tgz'"
+
+ #clean hook
+ clean:
+ dir: '{{.USER_WORKING_DIR}}'
+ ignore_error: true
+ cmds:
+ - dotnet clean -c release
+ - powershell -Command "Remove-Item -Recurse bin"
+ - powershell -Command "Remove-Item -Recurse obj" \ No newline at end of file
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
new file mode 100644
index 0000000..b0e8695
--- /dev/null
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
@@ -0,0 +1,175 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Security.Cryptography;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+
+using static NVault.Crypto.Secp256k1.LibSecp256k1;
+
+
+namespace NVault.Crypto.Secp256k1
+{
+ public static unsafe class ContextExtensions
+ {
+ /// <summary>
+ /// Creates a new <see cref="Secp256k1Context"/> from the current managed library
+ /// </summary>
+ /// <param name="Lib"></param>
+ /// <returns>The new <see cref="Secp256k1Context"/> object from the library</returns>
+ /// <exception cref="CryptographicException"></exception>
+ public static Secp256k1Context CreateContext(this LibSecp256k1 Lib)
+ {
+ //Protect for released lib
+ Lib.SafeLibHandle.ThrowIfClosed();
+
+ //Create new context
+ IntPtr context = Lib._create(1);
+
+ if (context == IntPtr.Zero)
+ {
+ throw new CryptographicException("Failed to create the new Secp256k1 context");
+ }
+
+ return new Secp256k1Context(Lib, context);
+ }
+
+ /// <summary>
+ /// Signs a 32byte message digest with the specified secret key on the current context and writes the signature to the specified buffer
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="secretKey">The 32byte secret key used to sign messages from the user</param>
+ /// <param name="digest">The 32byte message digest to compute the signature of</param>
+ /// <param name="signature">The buffer to write the signature output to, must be at-least 64 bytes</param>
+ /// <returns>The number of bytes written to the signature buffer, or less than 1 if the operation failed</returns>
+ public static ERRNO SchnorSignDigest(this in Secp256k1Context context, ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> digest, Span<byte> signature)
+ {
+ //Check the signature buffer size
+ if (signature.Length < SignatureSize)
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ //Message digest must be exactly 32 bytes long
+ if (digest.Length != (int)HashAlg.SHA256)
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ //Stack allocated keypair
+ KeyPair keyPair = new();
+
+ //Randomize the context and create the keypair
+ if (!context.CreateKeyPair(&keyPair, secretKey))
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ //Create the random nonce
+ byte* random = stackalloc byte[RandomBufferSize];
+
+ //Fill the buffer with random bytes
+ context.Lib.GetRandomBytes(new Span<byte>(random, RandomBufferSize));
+
+ try
+ {
+ fixed (byte* sigPtr = signature, digestPtr = digest)
+ {
+ //Sign the message hash and write the output to the signature buffer
+ if (context.Lib._signHash(context.Context, sigPtr, digestPtr, &keyPair, random) != 1)
+ {
+ return ERRNO.E_FAIL;
+ }
+ }
+ }
+ finally
+ {
+ //Erase entropy
+ MemoryUtil.InitializeBlock(random, RandomBufferSize);
+
+ //Clear the keypair, contains the secret key, even if its stack allocated
+ MemoryUtil.ZeroStruct(&keyPair);
+ }
+
+ //Signature size is always 64 bytes
+ return SignatureSize;
+ }
+
+ /// <summary>
+ /// Generates an x-only Schnor encoded public key from the specified secret key on the
+ /// current context and writes it to the specified buffer.
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="secretKey">The 32byte secret key used to derrive the public key from</param>
+ /// <param name="pubKeyBuffer">The buffer to write the x-only Schnor encoded public key</param>
+ /// <returns>The number of bytes written to the output buffer, or 0 if the operation failed</returns>
+ /// <exception cref="CryptographicException"></exception>
+ public static ERRNO GeneratePubKeyFromSecret(this in Secp256k1Context context, ReadOnlySpan<byte> secretKey, Span<byte> pubKeyBuffer)
+ {
+ if (secretKey.Length != SecretKeySize)
+ {
+ throw new CryptographicException($"Your secret key must be exactly {SecretKeySize} bytes long");
+ }
+
+ if (pubKeyBuffer.Length < XOnlyPublicKeySize)
+ {
+ throw new CryptographicException($"Your public key buffer must be at least {XOnlyPublicKeySize} bytes long");
+ }
+
+ //Protect for released lib
+ context.Lib.SafeLibHandle.ThrowIfClosed();
+
+ //Stack allocated keypair and x-only public key
+ XOnlyPubKey xOnlyPubKey = new();
+ KeyPair keyPair = new();
+
+ try
+ {
+ //Init context and keypair
+ if (!context.CreateKeyPair(&keyPair, secretKey))
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ //X-only public key from the keypair
+ if (context.Lib._createXonly(context.Context, &xOnlyPubKey, 0, &keyPair) != 1)
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ fixed (byte* pubBuffer = pubKeyBuffer)
+ {
+ //Serialize the public key to the buffer as an X-only public key without leading status byte
+ if (context.Lib._serializeXonly(context.Context, pubBuffer, &xOnlyPubKey) != 1)
+ {
+ return ERRNO.E_FAIL;
+ }
+ }
+ }
+ finally
+ {
+ //Clear the keypair, contains the secret key, even if its stack allocated
+ MemoryUtil.ZeroStruct(&keyPair);
+ }
+
+ //PubKey length is constant
+ return XOnlyPublicKeySize;
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/IRandomSource.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/IRandomSource.cs
new file mode 100644
index 0000000..542fc9c
--- /dev/null
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/IRandomSource.cs
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+
+namespace NVault.Crypto.Secp256k1
+{
+ /// <summary>
+ /// Represents a generator for random data, that fills abinary buffer with random bytes
+ /// on demand.
+ /// </summary>
+ public interface IRandomSource
+ {
+ /// <summary>
+ /// Fills the given buffer with random bytes
+ /// </summary>
+ /// <param name="buffer">Binary buffer to fill with random data</param>
+ void GetRandomBytes(Span<byte> buffer);
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
new file mode 100644
index 0000000..2eea450
--- /dev/null
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
@@ -0,0 +1,209 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Runtime.InteropServices;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Native;
+using VNLib.Utils.Extensions;
+
+namespace NVault.Crypto.Secp256k1
+{
+
+ public unsafe class LibSecp256k1 : VnDisposeable
+ {
+ public const int SecretKeySize = 32;
+ public const int XOnlyPublicKeySize = 32;
+ public const int SignatureSize = 64;
+ public const int KeyPairSize = 96;
+ public const int RandomBufferSize = 32;
+
+ /*
+ * Unsafe structures that represent the native keypair and x-only public key
+ * structures. They hold character arrays
+ */
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct KeyPair
+ {
+ public fixed byte data[Length];
+ public const int Length = KeyPairSize;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct XOnlyPubKey
+ {
+ public fixed byte data[Length];
+ public const int Length = 64;
+ }
+
+ //Native methods
+ [SafeMethodName("secp256k1_context_create")]
+ internal delegate IntPtr CreateContext(int flags);
+
+ [SafeMethodName("secp256k1_context_destroy")]
+ internal delegate void DestroyContext(IntPtr context);
+
+ [SafeMethodName("secp256k1_context_randomize")]
+ internal delegate int RandomizeContext(IntPtr context, byte* seed32);
+
+ [SafeMethodName("secp256k1_keypair_create")]
+ internal delegate int KeypairCreate(IntPtr context, KeyPair* keyPair, byte* secretKey);
+
+ [SafeMethodName("secp256k1_keypair_xonly_pub")]
+ internal delegate int KeypairXOnlyPub(IntPtr ctx, XOnlyPubKey* pubkey, int pk_parity, KeyPair* keypair);
+
+ [SafeMethodName("secp256k1_xonly_pubkey_serialize")]
+ internal delegate int XOnlyPubkeySerialize(IntPtr ctx, byte* output32, XOnlyPubKey* pubkey);
+
+ [SafeMethodName("secp256k1_schnorrsig_sign32")]
+ internal delegate int SignHash(IntPtr ctx, byte* sig64, byte* msg32, KeyPair* keypair, byte* aux_rand32);
+
+ /// <summary>
+ /// Loads the Secp256k1 library from the specified path and creates a wrapper class (loads methods from the library)
+ /// </summary>
+ /// <param name="dllPath">The realtive or absolute path to the shared library</param>
+ /// <param name="search">The DLL probing path pattern</param>
+ /// <returns>The <see cref="LibSecp256k1"/> library wrapper class</returns>
+ /// <exception cref="DllNotFoundException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="MissingMemberException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static LibSecp256k1 LoadLibrary(string dllPath, DllImportSearchPath search, IRandomSource? random)
+ {
+ _ = dllPath?? throw new ArgumentNullException(nameof(dllPath));
+
+ //try to load the library
+ SafeLibraryHandle lib = SafeLibraryHandle.LoadLibrary(dllPath, search);
+
+ //try to create the wrapper class, if it fails, dispose the library
+ try
+ {
+ //setup fallback random source if null
+ random ??= new FallbackRandom();
+
+ //Create the lib
+ return new LibSecp256k1(lib, random);
+ }
+ catch
+ {
+ //Dispose the library if the creation failed
+ lib.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// The underlying library handle
+ /// </summary>
+ public SafeLibraryHandle SafeLibHandle { get; }
+
+ internal readonly KeypairCreate _createKeyPair;
+ internal readonly CreateContext _create;
+ internal readonly RandomizeContext _randomize;
+ internal readonly DestroyContext _destroy;
+ internal readonly KeypairXOnlyPub _createXonly;
+ internal readonly XOnlyPubkeySerialize _serializeXonly;
+ internal readonly SignHash _signHash;
+ private readonly IRandomSource _randomSource;
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="LibSecp256k1"/> class from the specified library handle
+ /// </summary>
+ /// <param name="handle">The library handle that referrences the secp256k1 platform specific library</param>
+ /// <remarks>
+ /// This method attempts to capture all the native methods from the library, which may throw if the library is not valid.
+ /// </remarks>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="MissingMemberException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public LibSecp256k1(SafeLibraryHandle handle, IRandomSource randomSource)
+ {
+ //Store library handle
+ SafeLibHandle = handle ?? throw new ArgumentNullException(nameof(handle));
+
+ //Get all method handles and store them
+ _create = handle.DangerousGetMethod<CreateContext>();
+ _createKeyPair = handle.DangerousGetMethod<KeypairCreate>();
+ _randomize = handle.DangerousGetMethod<RandomizeContext>();
+ _destroy = handle.DangerousGetMethod<DestroyContext>();
+ _createXonly = handle.DangerousGetMethod<KeypairXOnlyPub>();
+ _serializeXonly = handle.DangerousGetMethod<XOnlyPubkeySerialize>();
+ _signHash = handle.DangerousGetMethod<SignHash>();
+
+ //Store random source
+ _randomSource = randomSource;
+ }
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="LibSecp256k1"/> class from the specified library handle
+ /// with a fallback random source
+ /// </summary>
+ /// <param name="handle">The library handle</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="MissingMemberException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public LibSecp256k1(SafeLibraryHandle handle):this(handle, new FallbackRandom())
+ {}
+
+ /// <summary>
+ /// Generates a new secret key and writes it to the specified buffer. The buffer size must be exactly <see cref="SecretKeySize"/> bytes long
+ /// </summary>
+ /// <param name="buffer">The secret key buffer</param>
+ /// <exception cref="ArgumentException"></exception>
+ public void CreateSecretKey(Span<byte> buffer)
+ {
+ //Protect for released lib
+ SafeLibHandle.ThrowIfClosed();
+
+ if(buffer.Length != SecretKeySize)
+ {
+ throw new ArgumentException($"Buffer must be exactly {SecretKeySize} bytes long", nameof(buffer));
+ }
+
+ //Fill the buffer with random bytes
+ _randomSource.GetRandomBytes(buffer);
+ }
+
+ /// <summary>
+ /// Fills the given buffer with random bytes from
+ /// the internal random source
+ /// </summary>
+ /// <param name="buffer">The buffer to fill with random data</param>
+ public void GetRandomBytes(Span<byte> buffer)
+ {
+ //Protect for released lib
+ SafeLibHandle.ThrowIfClosed();
+
+ _randomSource.GetRandomBytes(buffer);
+ }
+
+ protected override void Free()
+ {
+ //Free native library
+ SafeLibHandle.Dispose();
+ }
+
+ private record class FallbackRandom : IRandomSource
+ {
+ public void GetRandomBytes(Span<byte> buffer)
+ {
+ //Use the random generator from the crypto lib
+ RandomHash.GetRandomBytes(buffer);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj b/back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj
new file mode 100644
index 0000000..fd64768
--- /dev/null
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <RootNamespace>NVault.Crypto.Secp256k1</RootNamespace>
+ <AssemblyName>NVault.Crypto.Secp256k1</AssemblyName>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>NVault.Crypto.Secp256k1</Product>
+ <Description>Provides a managed library for the Bitcoin Core secp256k1 library, along with other helper types for NVault</Description>
+ <Copyright>Copyright © 2023 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/NVault</PackageProjectUrl>
+ <RepositoryUrl>https://github.com/VnUgE/NVault/tree/master/</RepositoryUrl>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="VNLib.Hashing.Portable" Version="0.1.0-ci0070" />
+ <PackageReference Include="VNLib.Utils" Version="0.1.0-ci0070" />
+ </ItemGroup>
+
+</Project>
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs
new file mode 100644
index 0000000..8aac0b8
--- /dev/null
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs
@@ -0,0 +1,79 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory;
+
+using static NVault.Crypto.Secp256k1.LibSecp256k1;
+
+namespace NVault.Crypto.Secp256k1
+{
+ /// <summary>
+ /// Represents a Secp256k1 context, it is used to randomize the instance, create key pairs,
+ /// and frees the context when disposed
+ /// </summary>
+ /// <param name="Lib">The <see cref="LibSecp256k1"/> library instance</param>
+ /// <param name="Context">A pointer to the initialized context instance</param>
+ public readonly record struct Secp256k1Context(LibSecp256k1 Lib, IntPtr Context) : IDisposable
+ {
+ /// <summary>
+ /// Randomizes the context with random data using the library's random source
+ /// </summary>
+ /// <returns>True if the context was successfully randomized, false otherwise</returns>
+ public unsafe readonly bool Randomize()
+ {
+ Lib.SafeLibHandle.ThrowIfClosed();
+
+ //Randomze the context
+ byte* entropy = stackalloc byte[RandomBufferSize];
+
+ //Get random bytes
+ Lib.GetRandomBytes(new Span<byte>(entropy, RandomBufferSize));
+
+ //call native randomize method
+ bool result = Lib._randomize(Context, entropy) == 1;
+
+ //Zero the randomness buffer before returning to avoid leaking random data
+ MemoryUtil.InitializeBlock(entropy, RandomBufferSize);
+
+ return result;
+ }
+
+ internal unsafe readonly bool CreateKeyPair(KeyPair* keyPair, ReadOnlySpan<byte> secretKey)
+ {
+ Lib.SafeLibHandle.ThrowIfClosed();
+
+ fixed (byte* sk = secretKey)
+ {
+ //Create the keypair from the secret key
+ return Lib._createKeyPair(Context, keyPair, sk) == 1;
+ }
+ }
+
+ /// <summary>
+ /// Releases the context instance and frees the memory
+ /// </summary>
+ public readonly void Dispose()
+ {
+ if (Context != IntPtr.Zero)
+ {
+ //Free the context
+ Lib._destroy(Context);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs
new file mode 100644
index 0000000..d4d9a06
--- /dev/null
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs
@@ -0,0 +1,101 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Runtime.InteropServices;
+
+using VNLib.Utils;
+using VNLib.Utils.Native;
+using VNLib.Utils.Extensions;
+
+namespace NVault.Crypto.Secp256k1
+{
+
+ /// <summary>
+ /// A wrapper class for an unmanaged random source that conforms to the <see cref="IRandomSource"/> interface
+ /// </summary>
+ public class UnmanagedRandomSource : VnDisposeable, IRandomSource
+ {
+ public const string METHOD_NAME = "getRandomBytes";
+
+ unsafe delegate void UnmanagedRandomSourceDelegate(byte* buffer, int size);
+
+
+ private readonly bool OwnsHandle;
+ private readonly SafeLibraryHandle _library;
+ private readonly UnmanagedRandomSourceDelegate _getRandomBytes;
+
+ /// <summary>
+ /// Loads the unmanaged random source from the given library
+ /// and attempts to get the random bytes method <see cref="METHOD_NAME"/>
+ /// </summary>
+ /// <param name="path"></param>
+ /// <param name="search"></param>
+ /// <returns>The wrapped library that conforms to the <see cref="IRandomSource"/></returns>
+ public static UnmanagedRandomSource LoadLibrary(string path, DllImportSearchPath search)
+ {
+ //Try to load the library
+ SafeLibraryHandle lib = SafeLibraryHandle.LoadLibrary(path, search);
+ try
+ {
+ return new UnmanagedRandomSource(lib, true);
+ }
+ catch
+ {
+ //release lib
+ lib.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Creates the unmanaged random source from the given library
+ /// </summary>
+ /// <param name="lib">The library handle to wrap</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public UnmanagedRandomSource(SafeLibraryHandle lib, bool ownsHandle)
+ {
+ lib.ThrowIfClosed();
+
+ _library = lib;
+
+ //get the method delegate
+ _getRandomBytes = lib.DangerousGetMethod<UnmanagedRandomSourceDelegate>(METHOD_NAME);
+
+ OwnsHandle = ownsHandle;
+ }
+
+ public unsafe void GetRandomBytes(Span<byte> buffer)
+ {
+ _library.ThrowIfClosed();
+
+ //Fix buffer and call unmanaged method
+ fixed(byte* ptr = buffer)
+ {
+ _getRandomBytes(ptr, buffer.Length);
+ }
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ if (OwnsHandle)
+ {
+ _library.Dispose();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/IClientAccessScope.cs b/back-end/libs/NVault.VaultExtensions/src/IClientAccessScope.cs
new file mode 100644
index 0000000..7a83fd7
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/IClientAccessScope.cs
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Collections.Generic;
+
+namespace NVault.VaultExtensions
+{
+ /// <summary>
+ /// Represents a user auth token access scope
+ /// configuration.
+ /// </summary>
+ public interface IClientAccessScope
+ {
+ /// <summary>
+ /// The list of policies for new token generation
+ /// </summary>
+ IList<string> Policies { get; }
+
+ /// <summary>
+ /// Allows the user to renew the access token
+ /// </summary>
+ bool Renewable { get; }
+
+ /// <summary>
+ /// The token
+ /// </summary>
+ string TokenTtl { get; }
+
+ /// <summary>
+ /// The explicit number of token uses allowed by the genreated token,
+ /// 0 for unlimited uses
+ /// </summary>
+ int NumberOfUses { get; }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/IKvVaultStore.cs b/back-end/libs/NVault.VaultExtensions/src/IKvVaultStore.cs
new file mode 100644
index 0000000..261bd7c
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/IKvVaultStore.cs
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System.Threading.Tasks;
+
+using VNLib.Utils.Memory;
+
+namespace NVault.VaultExtensions
+{
+ /// <summary>
+ /// Represents a vault key-value store that can be used to store secrets
+ /// </summary>
+ public interface IKvVaultStore
+ {
+ /// <summary>
+ /// Deletes a secret from the vault
+ /// </summary>
+ /// <param name="user">The user scope of the secret</param>
+ /// <param name="path">The path to the secret</param>
+ /// <returns>A task that returns when the operation has completed</returns>
+ Task DeleteSecretAsync(VaultUserScope user, string path);
+
+ /// <summary>
+ /// Sets a secret in the vault at the specified path and user scope
+ /// </summary>
+ /// <param name="user">The user scope to store the value at</param>
+ /// <param name="path">The path to the secret</param>
+ /// <param name="secret">The secret value to set</param>
+ /// <returns>A task that resolves when the secret has been updated</returns>
+ Task SetSecretAsync(VaultUserScope user, string path, PrivateString secret);
+
+ /// <summary>
+ /// Gets a secret from the vault at the specified path and user scope
+ /// </summary>
+ /// <param name="user">The user scope to get the value from</param>
+ /// <param name="path">The secret path</param>
+ /// <returns>A task that resolves the secret if found, null otherwise</returns>
+ Task<PrivateString?> GetSecretAsync(VaultUserScope user, string path);
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/IVaultClientScope.cs b/back-end/libs/NVault.VaultExtensions/src/IVaultClientScope.cs
new file mode 100644
index 0000000..873a115
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/IVaultClientScope.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+namespace NVault.VaultExtensions
+{
+ /// <summary>
+ /// Represents a vault client scope configuration
+ /// </summary>
+ public interface IVaultClientScope
+ {
+ /// <summary>
+ /// The mount point for the vault
+ /// </summary>
+ string? MountPoint { get; }
+
+ /// <summary>
+ /// The entry path for the vault
+ /// </summary>
+ string? EntryPath { get; }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/IVaultKvClientScope.cs b/back-end/libs/NVault.VaultExtensions/src/IVaultKvClientScope.cs
new file mode 100644
index 0000000..951f5e2
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/IVaultKvClientScope.cs
@@ -0,0 +1,29 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+namespace NVault.VaultExtensions
+{
+ /// <summary>
+ /// A key-value specific scoped client
+ /// </summary>
+ public interface IVaultKvClientScope : IVaultClientScope
+ {
+ /// <summary>
+ /// The property to store the secret value in the
+ /// storage dictionary
+ /// </summary>
+ string StorageProperty { get; }
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/KvVaultStorage.cs b/back-end/libs/NVault.VaultExtensions/src/KvVaultStorage.cs
new file mode 100644
index 0000000..b679404
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/KvVaultStorage.cs
@@ -0,0 +1,66 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System.Threading.Tasks;
+
+using VaultSharp;
+
+using VNLib.Utils.Memory;
+
+namespace NVault.VaultExtensions
+{
+ /// <summary>
+ /// An abstract kv storage implementation that uses the vault client to store secrets
+ /// </summary>
+ public abstract class KvVaultStorage : IKvVaultStore
+ {
+ /// <summary>
+ /// The vault client
+ /// </summary>
+ protected abstract IVaultClient Client { get; }
+
+ /// <summary>
+ /// The storage scope
+ /// </summary>
+ protected abstract IVaultKvClientScope Scope { get; }
+
+ public virtual Task DeleteSecretAsync(VaultUserScope user, string path)
+ {
+ string tPath = TranslatePath(path);
+ return Client.DeleteSecretAsync(Scope, user, tPath);
+ }
+
+ public virtual Task SetSecretAsync(VaultUserScope user, string path, PrivateString secret)
+ {
+ string tPath = TranslatePath(path);
+ return Client.SetSecretAsync(Scope, user, tPath, secret);
+ }
+
+ public virtual Task<PrivateString?> GetSecretAsync(VaultUserScope user, string path)
+ {
+ string tPath = TranslatePath(path);
+ return Client.GetSecretAsync(Scope, user, tPath);
+ }
+
+ /// <summary>
+ /// Translates a realtive item path to a full path
+ /// within the scope of the storage. This may be used to
+ /// extend the scope of the operation
+ /// </summary>
+ /// <param name="path">The item path to scope</param>
+ /// <returns>The further scoped vault path for the item</returns>
+ public virtual string TranslatePath(string path) => path;
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj b/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
new file mode 100644
index 0000000..43f3e15
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <RootNamespace>NVault.VaultExtensions</RootNamespace>
+ <AssemblyName>NVault.VaultExtensions</AssemblyName>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>NVault.VaultExtensions</Product>
+ <Description>A Hashicorp Vault unified extension library for NVault</Description>
+ <Copyright>Copyright © 2023 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/NVault</PackageProjectUrl>
+ <RepositoryUrl>https://github.com/VnUgE/NVault/tree/master/</RepositoryUrl>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="VaultSharp" Version="1.13.0.1" />
+ <PackageReference Include="VNLib.Plugins.Extensions.Loading" Version="0.1.0-ci0033" />
+ </ItemGroup>
+
+</Project>
diff --git a/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs b/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs
new file mode 100644
index 0000000..5a7c637
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs
@@ -0,0 +1,156 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using VaultSharp;
+using VaultSharp.V1.Commons;
+
+using VNLib.Utils.Memory;
+using VNLib.Plugins.Essentials.Extensions;
+
+
+namespace NVault.VaultExtensions
+{
+
+ public static class VaultClientExtensions
+ {
+
+ private static string GetKeyPath(IVaultClientScope client, in VaultUserScope scope, string itemPath)
+ {
+ //Allow for null entry path
+ return client.EntryPath == null ? $"{scope.UserId}/{itemPath}" : $"{client.EntryPath}/{scope.UserId}/{itemPath}";
+ }
+
+
+ public static Task<PrivateString?> GetSecretAsync(this IVaultClient client, IVaultKvClientScope scope, VaultUserScope user, string path)
+ {
+ return GetSecretAsync(client, scope, user, path, scope.StorageProperty);
+ }
+
+ public static async Task<PrivateString?> GetSecretAsync(this IVaultClient client, IVaultClientScope scope, VaultUserScope user, string path, string property)
+ {
+ //Get the path complete path for the scope
+ string fullPath = GetKeyPath(scope, user, path);
+
+ //Get the secret from the vault
+ Secret<SecretData> result = await client.V1.Secrets.KeyValue.V2.ReadSecretAsync(fullPath, mountPoint:scope.MountPoint);
+
+ //Try to get the secret value from the store
+ string? value = result.Data.Data.GetValueOrDefault(property)?.ToString();
+
+ //Return the secret value as a private string
+ return value == null ? null : new PrivateString(value);
+ }
+
+ /// <summary>
+ /// Writes a secret to the vault that is scoped by the vault scope, and the user scope.
+ /// </summary>
+ /// <param name="client"></param>
+ /// <param name="scope">The client scope configuration</param>
+ /// <param name="user">The user scope to isolate the </param>
+ /// <param name="path">The item path within the current scope</param>
+ /// <param name="secret">The secret value to set at the desired property</param>
+ /// <returns>A task that resolves when the secret has been updated</returns>
+ public static async Task<CurrentSecretMetadata> SetSecretAsync(this IVaultClient client, IVaultKvClientScope scope, VaultUserScope user, string path, PrivateString secret)
+ {
+ Dictionary<string, string> secretDict = new()
+ {
+ //Dangerous cast, but we know the type
+ { scope.StorageProperty, (string)secret }
+ };
+
+ //Await the result so we be sure the secret is not destroyed
+ return await SetSecretAsync(client, scope, user, path, secretDict);
+ }
+
+ /// <summary>
+ /// Writes a secret to the vault that is scoped by the vault scope, and the user scope.
+ /// </summary>
+ /// <param name="client"></param>
+ /// <param name="scope">The client scope configuration</param>
+ /// <param name="user">The user scope to isolate the </param>
+ /// <param name="path">The item path within the current scope</param>
+ /// <param name="secret">The secret value to set at the desired property</param>
+ /// <returns>A task that resolves when the secret has been updated</returns>
+ public static async Task<CurrentSecretMetadata> SetSecretAsync(this IVaultClient client, IVaultClientScope scope, VaultUserScope user, string path, IDictionary<string, string> secret)
+ {
+ //Get the path complete path for the scope
+ string fullPath = GetKeyPath(scope, user, path);
+
+ //Get the secret from the vault
+ Secret<CurrentSecretMetadata> result = await client.V1.Secrets.KeyValue.V2.WriteSecretAsync(fullPath, secret, mountPoint:scope.MountPoint);
+
+ return result.Data;
+ }
+
+ /// <summary>
+ /// Deletes a secret from the vault that is scoped by the vault scope, and the user scope.
+ /// </summary>
+ /// <param name="client"></param>
+ /// <param name="scope">The client scope</param>
+ /// <param name="user">The vault user scope</param>
+ /// <param name="path">The path to the storage</param>
+ /// <returns>A task that resolves when the delete operation has completed</returns>
+ public static Task DeleteSecretAsync(this IVaultClient client, IVaultClientScope scope, VaultUserScope user, string path)
+ {
+ string fullApth = GetKeyPath(scope, user, path);
+ return client.V1.Secrets.KeyValue.V2.DeleteSecretAsync(fullApth, mountPoint:scope.MountPoint);
+ }
+
+ /// <summary>
+ /// Deletes a secret from the vault
+ /// </summary>
+ /// <param name="user">The user scope of the secret</param>
+ /// <param name="path">The path to the secret</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that returns when the operation has completed</returns>
+ public static Task DeleteSecretAsync(this IKvVaultStore store, VaultUserScope user, string path, CancellationToken cancellation)
+ {
+ return store.DeleteSecretAsync(user, path).WaitAsync(cancellation);
+ }
+
+
+ /// <summary>
+ /// Gets a secret from the vault at the specified path and user scope
+ /// </summary>
+ /// <param name="user">The user scope to get the value from</param>
+ /// <param name="path">The secret path</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that resolves the secret if found, null otherwise</returns>
+ public static Task<PrivateString?> GetSecretAsync(this IKvVaultStore store, VaultUserScope user, string path, CancellationToken cancellation)
+ {
+ return store.GetSecretAsync(user, path).WaitAsync(cancellation);
+ }
+
+
+ /// <summary>
+ /// Sets a secret in the vault at the specified path and user scope
+ /// </summary>
+ /// <param name="user">The user scope to store the value at</param>
+ /// <param name="path">The path to the secret</param>
+ /// <param name="secret">The secret value to set</param>
+ /// <param name="cancellation">The cancellation token</param>
+ /// <returns>A task that resolves when the secret has been updated</returns>
+ public static Task SetSecretAsync(this IKvVaultStore store, VaultUserScope user, string path, PrivateString secret, CancellationToken cancellation)
+ {
+ return store.SetSecretAsync(user, path, secret).WaitAsync(cancellation);
+ }
+
+
+ }
+} \ No newline at end of file
diff --git a/back-end/libs/NVault.VaultExtensions/src/VaultUserScope.cs b/back-end/libs/NVault.VaultExtensions/src/VaultUserScope.cs
new file mode 100644
index 0000000..b70028e
--- /dev/null
+++ b/back-end/libs/NVault.VaultExtensions/src/VaultUserScope.cs
@@ -0,0 +1,25 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+namespace NVault.VaultExtensions
+{
+ /// <summary>
+ /// Represents a user scope for the vault. It isolates the user's
+ /// secrets from other users.
+ /// </summary>
+ /// <param name="UserId">The id of the user to scope the vault to</param>
+ public readonly record struct VaultUserScope(string UserId)
+ { }
+} \ No newline at end of file
diff --git a/back-end/plugins/nvault/src/Base64KeyEncoder.cs b/back-end/plugins/nvault/src/Base64KeyEncoder.cs
new file mode 100644
index 0000000..6e852ef
--- /dev/null
+++ b/back-end/plugins/nvault/src/Base64KeyEncoder.cs
@@ -0,0 +1,31 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Buffers.Text;
+
+using VNLib.Utils;
+
+namespace NVault.Plugins.Vault
+{
+ class Base64KeyEncoder : INostrKeyEncoder
+ {
+ public int GetKeyBufferSize(ReadOnlySpan<char> keyData) => Base64.GetMaxEncodedToUtf8Length(keyData.Length);
+
+ public ERRNO DecodeKey(ReadOnlySpan<char> keyData, Span<byte> buffer) => VnEncoding.TryFromBase64Chars(keyData, buffer);
+
+ public string? EncodeKey(ReadOnlySpan<byte> buffer) => Convert.ToBase64String(buffer);
+ }
+}
diff --git a/back-end/plugins/nvault/src/Endpoints/Endpoint.cs b/back-end/plugins/nvault/src/Endpoints/Endpoint.cs
new file mode 100644
index 0000000..00309d1
--- /dev/null
+++ b/back-end/plugins/nvault/src/Endpoints/Endpoint.cs
@@ -0,0 +1,424 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using Microsoft.EntityFrameworkCore;
+
+using FluentValidation;
+
+using NVault.VaultExtensions;
+
+using VNLib.Plugins;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Validation;
+using VNLib.Plugins.Extensions.Loading.Sql;
+using VNLib.Plugins.Extensions.Data.Extensions;
+
+using NVault.Plugins.Vault.Model;
+
+namespace NVault.Plugins.Vault.Endpoints
+{
+
+ [ConfigurationName("endpoint")]
+ internal class Endpoint : ProtectedWebEndpoint
+ {
+ private static IValidator<NostrEvent> EventValidator { get; } = NostrEvent.GetValidator();
+ private static IValidator<NostrRelay> RelayValidator { get; } = NostrRelay.GetValidator();
+ private static IValidator<NostrKeyMeta> KeyMetaValidator { get; } = NostrKeyMeta.GetValidator();
+ private static IValidator<CreateKeyRequest> CreateKeyRequestValidator { get; } = CreateKeyRequest.GetValidator();
+
+ private readonly INostrOperations _vault;
+ private readonly NostrRelayStore _relays;
+ private readonly NostrKeyMetaStore _publicKeyStore;
+ private readonly bool AllowDelete;
+
+ public Endpoint(PluginBase plugin, IConfigScope config)
+ {
+ string? path = config["path"].GetString();
+ InitPathAndLog(path, plugin.Log);
+
+ AllowDelete = config.TryGetValue("allow_delete", out JsonElement adEl) && adEl.GetBoolean();
+
+
+ DbContextOptions options = plugin.GetContextOptions();
+
+ _relays = new NostrRelayStore(options);
+ _publicKeyStore = new NostrKeyMetaStore(options);
+ _vault = new NostrOpProvider(plugin);
+ }
+
+
+ protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity)
+ {
+ //Check the operation flag
+ if(entity.QueryArgs.IsArgumentSet("type", "getRelays"))
+ {
+ //Get all relays
+ List<NostrRelay> relays = _relays.ListRental.Rent();
+
+ await _relays.GetUserPageAsync(relays, entity.Session.UserID, 0, 100);
+
+ //Return all relays for the user
+ entity.CloseResponseJson(HttpStatusCode.OK, relays);
+
+ _relays.ListRental.Return(relays);
+
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Get pubkey
+ if (entity.QueryArgs.IsArgumentSet("type", "getKeys"))
+ {
+ List<NostrKeyMeta> keys = _publicKeyStore.ListRental.Rent();
+
+ //Get the first public key for the user
+ await _publicKeyStore.GetUserPageAsync(keys, entity.Session.UserID, 0, 100);
+
+ //Return all keys for the user
+ entity.CloseResponseJson(HttpStatusCode.OK, keys);
+
+ _publicKeyStore.ListRental.Return(keys);
+
+ return VfReturnType.VirtualSkip;
+ }
+
+ return VfReturnType.NotFound;
+ }
+
+ protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
+ {
+
+ //Get the operation argument
+ if(entity.QueryArgs.IsArgumentSet("type", "signEvent"))
+ {
+ ValErrWebMessage webm = new();
+
+ //Get the event
+ NostrEvent? ev = await entity.GetJsonFromFileAsync<NostrEvent>();
+
+ if(webm.Assert(ev != null, "Bad request"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Basic validate the message
+ if(!EventValidator.Validate(ev, webm))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Get the key metadata
+ NostrKeyMeta? keyMeta = await _publicKeyStore.GetSingleUserRecordAsync(ev.KeyId, entity.Session.UserID);
+ if(webm.Assert(keyMeta != null, "Key not found"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.NotFound, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //If no public key is set, use the key metadata
+ if(string.IsNullOrWhiteSpace(ev.PublicKey))
+ {
+ ev.PublicKey = keyMeta.Value;
+ }
+
+ //Event public key must match the key metadata
+ if(webm.Assert(keyMeta.Value.Equals(ev.PublicKey, StringComparison.OrdinalIgnoreCase), "Key mismatch"))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Create user scope
+ VaultUserScope scope = new(entity.Session.UserID);
+
+ //try to sign the event
+ bool result = await _vault.SignEventAsync(scope, keyMeta, ev, entity.EventCancellation);
+
+ if(webm.Assert(result, "Failed to sign nostr event"))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ webm.Result = ev;
+ webm.Success = true;
+
+ //Return the signed event
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ return VfReturnType.NotFound;
+ }
+
+ protected override async ValueTask<VfReturnType> PatchAsync(HttpEntity entity)
+ {
+ ValErrWebMessage webm = new();
+
+ //Check for relay update
+ if (entity.QueryArgs.IsArgumentSet("type", "relay"))
+ {
+ //Get the new relay item
+ NostrRelay? relay = await entity.GetJsonFromFileAsync<NostrRelay>();
+
+ if(webm.Assert(relay != null, "No relay specified"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Validate
+ if (!RelayValidator.Validate(relay, webm))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Cleanup relay message
+ relay.CleanupFromUser();
+
+ //Update or create the relay for the user
+ if(await _relays.CreateUserRecordAsync(relay, entity.Session.UserID))
+ {
+ webm.Result = "Successfully updated relay";
+ webm.Success = true;
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ webm.Result = "Failed to update relay";
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Allow updating key metdata
+ if(entity.QueryArgs.IsArgumentSet("type", "identity"))
+ {
+ //Get the key metadata
+ NostrKeyMeta? meta = await entity.GetJsonFromFileAsync<NostrKeyMeta>();
+
+ if(webm.Assert(meta != null, "No key metadata specified"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Validate the key metadata
+ if(!KeyMetaValidator.Validate(meta, webm))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ meta.CleanupFromUser();
+
+ //Get the original record
+ NostrKeyMeta? original = await _publicKeyStore.GetSingleUserRecordAsync(meta.Id, entity.Session.UserID);
+
+ if(webm.Assert(original != null, "Key metadata not found"))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Merge the metadata
+ original.Merge(meta);
+
+ //Update the key metadata for the user
+ if(await _publicKeyStore.UpdateUserRecordAsync(original, entity.Session.UserID))
+ {
+ webm.Result = "Successfully updated key metadata";
+ webm.Success = true;
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ webm.Result = "Failed to update key metadata";
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ return VfReturnType.NotFound;
+ }
+
+ protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity)
+ {
+ //Allow creating a new identity
+ if(entity.QueryArgs.IsArgumentSet("type", "identity"))
+ {
+ ValErrWebMessage webm = new();
+
+ CreateKeyRequest? request = await entity.GetJsonFromFileAsync<CreateKeyRequest>();
+
+ if(webm.Assert(request != null, "Invalid key request"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ if(!CreateKeyRequestValidator.Validate(request, webm))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //try to create the record for the user first
+ NostrKeyMeta newKey = new()
+ {
+ UserId = entity.Session.UserID,
+ UserName = request.UserName,
+ //Create temporary key
+ Value = Guid.NewGuid().ToString("n")
+ };
+
+ //Create the key metadata record before we generate the keypair
+ if(!await _publicKeyStore.CreateUserRecordAsync(newKey, entity.Session.UserID))
+ {
+ //Failed to create key metadata record
+ webm.Result = "Failed to create key";
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Create new user scope
+ VaultUserScope scope = new(entity.Session.UserID);
+
+ bool result;
+
+ if (string.IsNullOrWhiteSpace(request.ExistingKey))
+ {
+ //Create a new keypair/identity for the user
+ result = await _vault.CreateCredentialAsync(scope, newKey, entity.EventCancellation);
+ }
+ else
+ {
+ //From exising key
+ result = await _vault.CreateFromExistingAsync(scope, newKey, request.ExistingKey, entity.EventCancellation);
+ }
+
+ if (result == false)
+ {
+ //Delete the meta entry from the store
+ await _publicKeyStore.DeleteUserRecordAsync(newKey.Id, entity.Session.UserID);
+
+ webm.Result = "Failed to create new identity";
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ /*
+ * Update the key now that the vault has updated the entity.
+ *
+ * We dont care if this fails because the key is already created, it will
+ * just be empty, which is fine, the user can update/delete it later.
+ */
+ await _publicKeyStore.UpdateUserRecordAsync(newKey, entity.Session.UserID);
+
+ //Store the new key
+ webm.Result = newKey;
+ webm.Success = true;
+
+ //Return the new key info
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ return VfReturnType.NotFound;
+ }
+
+ protected override async ValueTask<VfReturnType> DeleteAsync(HttpEntity entity)
+ {
+ ValErrWebMessage webMessage = new ();
+
+ if(entity.QueryArgs.IsArgumentSet("type", "identity"))
+ {
+ if (webMessage.Assert(AllowDelete, "Deleting identies are now allowed"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.Forbidden, webMessage);
+ return VfReturnType.VirtualSkip;
+ }
+
+ if (!entity.QueryArgs.TryGetNonEmptyValue("key_id", out string? keyId))
+ {
+ webMessage.Result = "No key id specified";
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webMessage);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Get the key metadata
+ NostrKeyMeta? meta = await _publicKeyStore.GetSingleUserRecordAsync(keyId, entity.Session.UserID);
+
+ if (webMessage.Assert(meta != null, "Key metadata not found"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.NotFound, webMessage);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Delete the key from the vault
+ VaultUserScope scope = new(entity.Session.UserID);
+
+ //Delete the key from the vault
+ await _vault.DeleteCredentialAsync(scope, meta, entity.EventCancellation);
+
+ //Remove the key metadata
+ await _publicKeyStore.DeleteUserRecordAsync(keyId, entity.Session.UserID);
+
+ webMessage.Result = "Successfully deleted identity";
+ webMessage.Success = true;
+ entity.CloseResponseJson(HttpStatusCode.OK, webMessage);
+ return VfReturnType.VirtualSkip;
+ }
+
+ return VfReturnType.NotFound;
+ }
+
+ sealed class CreateKeyRequest
+ {
+ public string? UserName { get; set; }
+
+ public string? ExistingKey { get; set; }
+
+ public static IValidator<CreateKeyRequest> GetValidator()
+ {
+ InlineValidator<CreateKeyRequest> val = new();
+
+ val.RuleFor(r => r.UserName)
+ .NotEmpty()
+ .Length(1, 100)
+ .IllegalCharacters();
+
+ //If a user-key is specified, it must be 64 characters long hexadecimal
+ val.When(p => !string.IsNullOrWhiteSpace(p.ExistingKey), () =>
+ {
+ val.RuleFor(r => r.ExistingKey)
+ .Length(64)
+ .AlphaNumericOnly();
+ });
+
+ return val;
+ }
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/INostrCryptoProvider.cs b/back-end/plugins/nvault/src/INostrCryptoProvider.cs
new file mode 100644
index 0000000..805eb21
--- /dev/null
+++ b/back-end/plugins/nvault/src/INostrCryptoProvider.cs
@@ -0,0 +1,66 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+
+using VNLib.Utils;
+
+namespace NVault.Plugins.Vault
+{
+ internal interface INostrCryptoProvider
+ {
+ /// <summary>
+ /// Gets the size of the buffer required to hold a signature data
+ /// </summary>
+ /// <returns>The size of the required buffer</returns>
+ int GetSignatureBufferSize();
+
+ /// <summary>
+ /// Signs a message digest with the specified private key and writes
+ /// the signature to the specified buffer.
+ /// </summary>
+ /// <param name="key"></param>
+ /// <param name="digest"></param>
+ /// <param name="signatureBuffer"></param>
+ /// <returns>The number of bytes written to the signature buffer, 0 or less if the operation failed</returns>
+ ERRNO SignMessage(ReadOnlySpan<byte> key, ReadOnlySpan<byte> digest, Span<byte> signatureBuffer);
+
+ /// <summary>
+ /// Determines the exact size of the buffer required to hold a key pair during
+ /// creation
+ /// </summary>
+ /// <returns>The structure containing the exact sizes of the buffers to allocate</returns>
+ KeyBufferSizes GetKeyBufferSize();
+
+ /// <summary>
+ /// Generates a new key pair and writes the key outputs to the specified buffers.
+ /// The buffers will be at-leat the size of the values returned by <see cref="GetKeyBufferSize"/>
+ /// </summary>
+ /// <param name="publicKey">A buffer to write the public key to</param>
+ /// <param name="privateKey">A buffer to write the private key to</param>
+ /// <returns>True if the operation succeeded</returns>
+ bool TryGenerateKeyPair(Span<byte> publicKey, Span<byte> privateKey);
+
+ /// <summary>
+ /// Recovers the public key from the specified private key and writes it to the specified buffer
+ /// </summary>
+ /// <param name="privateKey">The readonly private key</param>
+ /// <param name="pubKey">The recovered public key</param>
+ /// <returns>True if the operation succeeded, false otherwise</returns>
+ bool RecoverPublicKey(ReadOnlySpan<byte> privateKey, Span<byte> pubKey);
+ }
+
+ readonly record struct KeyBufferSizes(int PrivateKeySize, int PublicKeySize);
+}
diff --git a/back-end/plugins/nvault/src/INostrKeyEncoder.cs b/back-end/plugins/nvault/src/INostrKeyEncoder.cs
new file mode 100644
index 0000000..a76ab19
--- /dev/null
+++ b/back-end/plugins/nvault/src/INostrKeyEncoder.cs
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+
+using VNLib.Utils;
+
+namespace NVault.Plugins.Vault
+{
+ /// <summary>
+ /// Converts secp256k1 private keys to and from strings to store in
+ /// in the vault storage
+ /// </summary>
+ internal interface INostrKeyEncoder
+ {
+ /// <summary>
+ /// Gets the size of the buffer required to hold the key data to decode
+ /// </summary>
+ /// <param name="keyData">The encoded key buffer</param>
+ /// <returns>The minum size of the buffer required to decode the key</returns>
+ int GetKeyBufferSize(ReadOnlySpan<char> keyData);
+
+ /// <summary>
+ /// Decodes the specified key data into the specified buffer
+ /// </summary>
+ /// <param name="keyData">The key character buffer</param>
+ /// <param name="buffer">The binary output buffer</param>
+ /// <returns>The number of bytes encoded, 0 or less if the operation failed</returns>
+ ERRNO DecodeKey(ReadOnlySpan<char> keyData, Span<byte> buffer);
+
+ /// <summary>
+ /// Encodes the specified key buffer into a string
+ /// </summary>
+ /// <param name="buffer">The key to encode</param>
+ /// <returns>The encoded string if possible</returns>
+ string? EncodeKey(ReadOnlySpan<byte> buffer);
+ }
+}
diff --git a/back-end/plugins/nvault/src/INostrOperations.cs b/back-end/plugins/nvault/src/INostrOperations.cs
new file mode 100644
index 0000000..b7d82a2
--- /dev/null
+++ b/back-end/plugins/nvault/src/INostrOperations.cs
@@ -0,0 +1,35 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+using NVault.Plugins.Vault.Model;
+
+using NVault.VaultExtensions;
+
+namespace NVault.Plugins.Vault
+{
+ internal interface INostrOperations
+ {
+ Task<bool> SignEventAsync(VaultUserScope scope, NostrKeyMeta keyMeta, NostrEvent evnt, CancellationToken cancellation);
+
+ Task<bool> CreateCredentialAsync(VaultUserScope scope, NostrKeyMeta newKey, CancellationToken cancellation);
+
+ Task<bool> CreateFromExistingAsync(VaultUserScope scope, NostrKeyMeta newKey, string hexKey, CancellationToken cancellation);
+
+ Task DeleteCredentialAsync(VaultUserScope scope, NostrKeyMeta key, CancellationToken cancellation);
+ }
+}
diff --git a/back-end/plugins/nvault/src/INostrVault.cs b/back-end/plugins/nvault/src/INostrVault.cs
new file mode 100644
index 0000000..37bc05e
--- /dev/null
+++ b/back-end/plugins/nvault/src/INostrVault.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+using NVault.VaultExtensions;
+
+using VNLib.Utils.Memory;
+
+namespace NVault.Plugins.Vault
+{
+ interface INostrVault
+ {
+ Task<PrivateString?> GetKeyAsync(VaultUserScope scope, string keyId, CancellationToken cancellation);
+
+ Task SetKeyAsync(VaultUserScope scope, string keyId, PrivateString keyData, CancellationToken cancellation);
+
+ Task DeleteKeyAsync(VaultUserScope scope, string keyId, CancellationToken cancellation);
+ }
+}
diff --git a/back-end/plugins/nvault/src/ManagedCryptoprovider.cs b/back-end/plugins/nvault/src/ManagedCryptoprovider.cs
new file mode 100644
index 0000000..b34f220
--- /dev/null
+++ b/back-end/plugins/nvault/src/ManagedCryptoprovider.cs
@@ -0,0 +1,81 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Text.Json;
+using System.Collections.Generic;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading;
+
+using NVault.Crypto.Secp256k1;
+
+namespace NVault.Plugins.Vault
+{
+ [ConfigurationName("crypto")]
+ internal class ManagedCryptoprovider : INostrCryptoProvider
+ {
+ private readonly INostrCryptoProvider _provider;
+
+ public ManagedCryptoprovider(PluginBase plugin, IConfigScope config)
+ {
+ IRandomSource? random = null;
+
+ //See if a random source is specified
+ if (config.TryGetValue("lib_random", out JsonElement randomel))
+ {
+ bool isManaged = randomel.GetProperty("lib_type").GetString() == "managed";
+ string path = randomel.GetProperty("lib_path").GetString() ?? throw new KeyNotFoundException("Missing required key 'lib_path' in 'lib_random' element");
+
+ if (isManaged)
+ {
+ //Load managed assembly, plugin will manage lifetime
+ random = plugin.LoadAssembly<IRandomSource>(path).Resource;
+ }
+ else
+ {
+ //load unmanaged lib
+ random = UnmanagedRandomSource.LoadLibrary(path, System.Runtime.InteropServices.DllImportSearchPath.SafeDirectories);
+
+ //Register for unload to cleanup unmanaged lib
+ _ = plugin.RegisterForUnload(((UnmanagedRandomSource)random).Dispose);
+ }
+ }
+
+ string nativePath = config["native_lib"].GetString()!;
+
+ //Load native library path
+ _provider = NativeSecp256k1Library.LoadLibrary(nativePath, random);
+ plugin.Log.Verbose("Loaded native Secp256k1 library from {path}", nativePath);
+ }
+
+ ///<inheritdoc/>
+ public int GetSignatureBufferSize() => _provider.GetSignatureBufferSize();
+
+ ///<inheritdoc/>
+ public ERRNO SignMessage(ReadOnlySpan<byte> key, ReadOnlySpan<byte> digest, Span<byte> signatureBuffer) => _provider.SignMessage(key, digest, signatureBuffer);
+
+ ///<inheritdoc/>
+ public KeyBufferSizes GetKeyBufferSize() => _provider.GetKeyBufferSize();
+
+ ///<inheritdoc/>
+ public bool TryGenerateKeyPair(Span<byte> publicKey, Span<byte> privateKey) => _provider.TryGenerateKeyPair(publicKey, privateKey);
+
+ ///<inheritdoc/>
+ public bool RecoverPublicKey(ReadOnlySpan<byte> privateKey, Span<byte> pubKey) => _provider.RecoverPublicKey(privateKey, pubKey);
+ }
+}
diff --git a/back-end/plugins/nvault/src/ManagedVaultClient.cs b/back-end/plugins/nvault/src/ManagedVaultClient.cs
new file mode 100644
index 0000000..ff7977c
--- /dev/null
+++ b/back-end/plugins/nvault/src/ManagedVaultClient.cs
@@ -0,0 +1,71 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading;
+
+using VaultSharp;
+using VaultSharp.V1.AuthMethods.Token;
+
+using NVault.VaultExtensions;
+
+namespace NVault.Plugins.Vault
+{
+ [ConfigurationName("nostr_vault")]
+ internal sealed class ManagedVaultClient : KvVaultStorage
+ {
+ protected override IVaultClient Client { get; }
+ protected override IVaultKvClientScope Scope { get; }
+
+ public ManagedVaultClient(PluginBase plugin, IConfigScope config)
+ {
+ Scope = new KvScope()
+ {
+ MountPoint = config["mount"].GetString(),
+ EntryPath = config["entry"].GetString() ?? "nostr",
+ //Keys are stored in the key property
+ StorageProperty = "key"
+ };
+
+ //Create the client
+ Client = CreateClient(config);
+ }
+
+ private static IVaultClient CreateClient(IConfigScope config)
+ {
+ TokenAuthMethodInfo am = new(config["token"].GetString());
+
+ VaultClientSettings settings = new(config["url"].GetString(), am)
+ { };
+
+ return new VaultClient(settings);
+ }
+
+ ///<inheritdoc/>
+ public override string TranslatePath(string path)
+ {
+ //Prefix with the keys file path
+ return $"keys/{path}";
+ }
+
+
+ private sealed class KvScope : IVaultKvClientScope
+ {
+ public string StorageProperty { get; init; } = "";
+ public string? MountPoint { get; init; }
+ public string? EntryPath { get; init; }
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/Model/NostrContext.cs b/back-end/plugins/nvault/src/Model/NostrContext.cs
new file mode 100644
index 0000000..b70a38e
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrContext.cs
@@ -0,0 +1,100 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Loading.Sql;
+
+namespace NVault.Plugins.Vault.Model
+{
+
+ internal class NostrContext : TransactionalDbContext, IDbTableDefinition
+ {
+ public DbSet<NostrRelay> Relays { get; set; }
+
+ public DbSet<NostrKeyMeta> PublicKeys { get; set; }
+
+ public NostrContext()
+ { }
+
+ public NostrContext(DbContextOptions options) : base(options)
+ { }
+
+ public void OnDatabaseCreating(IDbContextBuilder builder, object? userState)
+ {
+ //Configure relay table
+ builder.DefineTable<NostrRelay>(nameof(Relays))
+ .WithColumn(r => r.Id)
+ .MaxLength(50)
+ .Next()
+
+ .WithColumn(r => r.UserId)
+ .Next()
+
+ //Url is unique
+ .WithColumn(r => r.Url)
+ .Unique()
+ .AllowNull(false)
+ .Next()
+
+ //Default flags is 0 (none)
+ .WithColumn(r => (int)r.Flags)
+ .AllowNull(false)
+ .WithDefault(0)
+ .Next()
+
+ .WithColumn(r => r.Created)
+ .AllowNull(false)
+ .Next()
+
+ .WithColumn(r => r.LastModified)
+ .AllowNull(false)
+ .Next()
+
+ //Finally, version, it should be set to the timestamp from annotations
+ .WithColumn(r => r.Version);
+
+ //Setup public key table
+ builder.DefineTable<NostrKeyMeta>(nameof(PublicKeys))
+ .WithColumn(r => r.Id)
+ .Next()
+
+ .WithColumn(r => r.UserId)
+ .Next()
+
+ .WithColumn(r => r.UserName)
+ .AllowNull(true)
+ .Next()
+
+ //Public key is unique
+ .WithColumn(r => r.Value)
+ .Unique()
+ .AllowNull(false)
+ .Next()
+
+ .WithColumn(r => r.Created)
+ .AllowNull(false)
+ .Next()
+
+ .WithColumn(r => r.LastModified)
+ .AllowNull(false)
+ .Next()
+
+ //Finally, version, it should be set to the timestamp from annotations
+ .WithColumn(r => r.Version);
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/Model/NostrEvent.cs b/back-end/plugins/nvault/src/Model/NostrEvent.cs
new file mode 100644
index 0000000..ccd5a2c
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrEvent.cs
@@ -0,0 +1,90 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System.Text.Json.Serialization;
+
+using FluentValidation;
+
+using VNLib.Plugins.Extensions.Validation;
+
+
+namespace NVault.Plugins.Vault.Model
+{
+ internal sealed class NostrEvent
+ {
+ public const int MAX_CONTENT_LENGTH = 16 * 1024;
+
+ [JsonPropertyName("KeyId")]
+ public string? KeyId { get; set; }
+
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("pubkey")]
+ public string? PublicKey { get; set; }
+
+ [JsonPropertyName("created_at")]
+ public long? Timestamp { get; set; }
+
+ [JsonPropertyName("kind")]
+ public NostrMessageKind? MessageKind { get; set; }
+
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ [JsonPropertyName("sig")]
+ public string? Signature { get; set; }
+
+ [JsonPropertyName("tags")]
+ public string[]?[]? Tags { get; set; }
+
+ public static IValidator<NostrEvent> GetValidator()
+ {
+ InlineValidator<NostrEvent> val = new();
+
+ //Event id should be empty, we will generate it while computing the hash
+ val.RuleFor(ev => ev.Id)
+ //ids are 32 bytes hex encoded
+ .Length(64)
+ .AlphaNumericOnly();
+
+ //No signature set
+ val.RuleFor(ev => ev.Signature)
+ .Empty();
+
+ //If pubkey is defined, must set a 64 byte hex encoded string
+ val.RuleFor(ev => ev.PublicKey!)
+ .AlphaNumericOnly()
+ .When(ev => ev.PublicKey != null)
+ .Length(64);
+
+ val.RuleFor(ev => ev.Content)
+ .Length(0, MAX_CONTENT_LENGTH);
+
+ val.RuleFor(ev => ev.Content)
+ //Content must be specifed when kind is a text note
+ .NotEmpty()
+ .When(ev => ev.MessageKind == NostrMessageKind.TextNote);
+
+ //Must have a key identity
+ val.RuleFor(ev => ev.KeyId)
+ .NotEmpty()
+ .Length(12, 64)
+ .AlphaNumericOnly();
+
+ return val;
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/plugins/nvault/src/Model/NostrKeyMeta.cs b/back-end/plugins/nvault/src/Model/NostrKeyMeta.cs
new file mode 100644
index 0000000..2e18ba7
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrKeyMeta.cs
@@ -0,0 +1,89 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+
+using FluentValidation;
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+using VNLib.Plugins.Extensions.Validation;
+
+namespace NVault.Plugins.Vault.Model
+{
+ internal class NostrKeyMeta : DbModelBase, IUserEntity
+ {
+ [Key]
+ [MaxLength(50)]
+ public override string Id { get; set; }
+ public override DateTime Created { get; set; }
+ public override DateTime LastModified { get; set; }
+
+ [JsonPropertyName("PublicKey")]
+ [MaxLength(500)]
+ public string? Value { get; set; }
+
+ [JsonIgnore]
+ [MaxLength(50)]
+ public string? UserId { get; set; }
+
+ [MaxLength(100)]
+ public string? UserName { get; set; }
+
+ public void CleanupFromUser()
+ {
+ //Forbidden fields
+ Created = DateTime.MinValue;
+ LastModified = DateTime.MinValue;
+ UserId = null;
+
+ //User is not allowed to change the key value
+ Value = null;
+
+ //Trim up username
+ UserName = UserName?.Trim();
+ }
+
+ public void Merge(NostrKeyMeta other)
+ {
+ if (other == null)
+ throw new ArgumentNullException(nameof(other));
+
+ //We only update username and key value
+ UserName = other.UserName;
+ }
+
+ public static IValidator<NostrKeyMeta> GetValidator()
+ {
+ InlineValidator<NostrKeyMeta> val = new();
+
+ val.RuleFor(r => r.Id)
+ .NotEmpty()
+ //Max id length is 64 hex characters
+ .MaximumLength(64)
+ //must only be hex characters
+ .AlphaNumericOnly();
+
+ val.RuleFor(r => r.UserName)
+ .NotEmpty()
+ .Length(1, 100)
+ .IllegalCharacters();
+
+ return val;
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/Model/NostrKeyMetaStore.cs b/back-end/plugins/nvault/src/Model/NostrKeyMetaStore.cs
new file mode 100644
index 0000000..96d0a3c
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrKeyMetaStore.cs
@@ -0,0 +1,79 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Linq;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace NVault.Plugins.Vault.Model
+{
+ internal sealed class NostrKeyMetaStore : DbStore<NostrKeyMeta>
+ {
+ private readonly DbContextOptions _options;
+
+ public NostrKeyMetaStore(DbContextOptions options) => _options = options;
+
+ ///<inheritdoc/>
+ public override IDbQueryLookup<NostrKeyMeta> QueryTable { get; } = new DbQueries();
+
+ ///<inheritdoc/>
+ public override IDbContextHandle GetNewContext() => new NostrContext(_options);
+
+ ///<inheritdoc/>
+ public override string GetNewRecordId() => Guid.NewGuid().ToString("N");
+
+ ///<inheritdoc/>
+ public override void OnRecordUpdate(NostrKeyMeta newRecord, NostrKeyMeta currentRecord)
+ {
+ //Update username and key value
+ currentRecord.UserName = newRecord.UserName;
+ currentRecord.Value = newRecord.Value;
+
+ currentRecord.UserId = newRecord.UserId;
+
+ //Update last modified time
+ newRecord.LastModified = DateTime.UtcNow;
+ }
+
+ sealed record class DbQueries : IDbQueryLookup<NostrKeyMeta>
+ {
+ ///<inheritdoc/>
+ public IQueryable<NostrKeyMeta> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints)
+ {
+ string userId = constraints[0];
+
+ return from r in context.Set<NostrKeyMeta>()
+ where r.UserId == userId
+ select r;
+ }
+
+ ///<inheritdoc/>
+ public IQueryable<NostrKeyMeta> GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints)
+ {
+ string id = constraints[0];
+ string userId = constraints[1];
+
+ //Get relay for the given user by its id
+ return from r in context.Set<NostrKeyMeta>()
+ where r.Id == id && r.UserId == userId
+ select r;
+ }
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/Model/NostrRelay.cs b/back-end/plugins/nvault/src/Model/NostrRelay.cs
new file mode 100644
index 0000000..e80d268
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrRelay.cs
@@ -0,0 +1,86 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Text.Json.Serialization;
+using System.ComponentModel.DataAnnotations;
+
+using FluentValidation;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+using VNLib.Plugins.Extensions.Validation;
+
+namespace NVault.Plugins.Vault.Model
+{
+ [Index(nameof(Url))]
+ internal class NostrRelay : DbModelBase, IUserEntity
+ {
+ [Key]
+ [MaxLength(50)]
+ [JsonPropertyName("id")]
+ public override string Id { get; set; }
+ public override DateTime Created { get; set; }
+ public override DateTime LastModified { get; set; }
+
+ [JsonPropertyName("url")]
+ [MaxLength(200)]
+ public string Url { get; set; }
+
+ [JsonPropertyName("flags")]
+ public NostrRelayFlags Flags { get; set; }
+
+ [JsonIgnore]
+ [MaxLength(50)]
+ public string UserId { get; set; }
+
+ public void CleanupFromUser()
+ {
+ //Forbidden fields
+ Created = DateTime.MinValue;
+ LastModified = DateTime.MinValue;
+ UserId = null;
+
+ //trim up url
+ Url = Url?.Trim();
+ }
+
+ public static IValidator<NostrRelay> GetValidator()
+ {
+ InlineValidator<NostrRelay> val = new();
+
+ //Must specify a relay id
+ val.RuleFor(r => r.Id)
+ .NotEmpty()
+ //Must be length 64 hex characters
+ .MaximumLength(50)
+ //must only be hex characters
+ .AlphaNumericOnly();
+
+ val.RuleFor(r => r.Url)
+ .NotEmpty()
+ .Length(5, 200)
+ .IllegalCharacters();
+
+ //Must set read/write flag, may be 0 for none/not allowed
+ val.RuleFor(r => r.Flags)
+ .NotEmpty();
+
+ return val;
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/Model/NostrRelayFlags.cs b/back-end/plugins/nvault/src/Model/NostrRelayFlags.cs
new file mode 100644
index 0000000..352ff11
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrRelayFlags.cs
@@ -0,0 +1,24 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+namespace NVault.Plugins.Vault.Model
+{
+ internal enum NostrRelayFlags
+ {
+ None = 0,
+ Read = 1,
+ Write = 2,
+ }
+}
diff --git a/back-end/plugins/nvault/src/Model/NostrRelayStore.cs b/back-end/plugins/nvault/src/Model/NostrRelayStore.cs
new file mode 100644
index 0000000..42f48ab
--- /dev/null
+++ b/back-end/plugins/nvault/src/Model/NostrRelayStore.cs
@@ -0,0 +1,78 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Linq;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+namespace NVault.Plugins.Vault.Model
+{
+ internal class NostrRelayStore : DbStore<NostrRelay>
+ {
+ private readonly DbContextOptions _options;
+
+ public NostrRelayStore(DbContextOptions options)
+ {
+ _options = options;
+ }
+
+ ///<inheritdoc/>
+ public override IDbQueryLookup<NostrRelay> QueryTable { get; } = new DbQueries();
+
+ ///<inheritdoc/>
+ public override IDbContextHandle GetNewContext() => new NostrContext(_options);
+
+ ///<inheritdoc/>
+ public override string GetNewRecordId() => Guid.NewGuid().ToString("N");
+
+
+ public override void OnRecordUpdate(NostrRelay newRecord, NostrRelay currentRecord)
+ {
+ currentRecord.Flags = newRecord.Flags;
+ currentRecord.Url = newRecord.Url;
+ currentRecord.UserId = newRecord.UserId;
+
+ //Update times
+ newRecord.LastModified = DateTime.UtcNow;
+ }
+
+ sealed record class DbQueries() : IDbQueryLookup<NostrRelay>
+ {
+ public IQueryable<NostrRelay> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints)
+ {
+ string userId = constraints[0];
+
+ return from r in context.Set<NostrRelay>()
+ where r.UserId == userId
+ select r;
+ }
+
+ public IQueryable<NostrRelay> GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints)
+ {
+ string id = constraints[0];
+ string userId = constraints[1];
+
+ //Get relay for the given user by its id
+ return from r in context.Set<NostrRelay>()
+ where r.Id == id && r.UserId == userId
+ select r;
+ }
+ }
+ }
+}
diff --git a/back-end/plugins/nvault/src/NVault.csproj b/back-end/plugins/nvault/src/NVault.csproj
new file mode 100644
index 0000000..a507418
--- /dev/null
+++ b/back-end/plugins/nvault/src/NVault.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <RootNamespace>NVault.Plugins.Vault</RootNamespace>
+ <AssemblyName>NVault</AssemblyName>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>Nvault</Product>
+ <Description>A VNLib.Plugins.Essentials framework plugin that provides a nostr vault backend called NVault</Description>
+ <Copyright>Copyright © 2023 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/NVault</PackageProjectUrl>
+ <RepositoryUrl>https://github.com/VnUgE/NVault/tree/master/</RepositoryUrl>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="FluentValidation" Version="11.7.1" />
+ <PackageReference Include="VNLib.Plugins.Extensions.Data" Version="0.1.0-ci0033" />
+ <PackageReference Include="VNLib.Plugins.Extensions.Validation" Version="0.1.0-ci0033" />
+ <PackageReference Include="VVNLib.Plugins.Extensions.Loading.Sql" Version="0.1.0-ci0033" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\libs\NVault.Crypto.Secp256k1\src\NVault.Crypto.Secp256k1.csproj" />
+ <ProjectReference Include="..\..\..\libs\NVault.VaultExtensions\src\NVault.VaultExtensions.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/back-end/plugins/nvault/src/NativeSecp256k1Library.cs b/back-end/plugins/nvault/src/NativeSecp256k1Library.cs
new file mode 100644
index 0000000..67143fc
--- /dev/null
+++ b/back-end/plugins/nvault/src/NativeSecp256k1Library.cs
@@ -0,0 +1,109 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+
+using NVault.Crypto.Secp256k1;
+
+using VNLib.Utils;
+
+namespace NVault.Plugins.Vault
+{
+ internal sealed class NativeSecp256k1Library : VnDisposeable, INostrCryptoProvider
+ {
+ private readonly LibSecp256k1 _lib;
+
+ private NativeSecp256k1Library(LibSecp256k1 lib)
+ {
+ _lib = lib;
+ }
+
+ /// <summary>
+ /// Loads the native library from the specified path
+ /// </summary>
+ /// <param name="libFilePath">The library path</param>
+ /// <param name="random">The optional random source</param>
+ /// <returns>The loaded <see cref="NativeSecp256k1Library"/></returns>
+ public static NativeSecp256k1Library LoadLibrary(string libFilePath, IRandomSource? random)
+ {
+ LibSecp256k1 lib = LibSecp256k1.LoadLibrary(libFilePath, System.Runtime.InteropServices.DllImportSearchPath.SafeDirectories, random);
+ return new(lib);
+ }
+
+ //Key sizes are constant
+ ///<inheritdoc/>
+ public KeyBufferSizes GetKeyBufferSize() => new(LibSecp256k1.SecretKeySize, LibSecp256k1.XOnlyPublicKeySize);
+
+ //Signature sizes are constant
+ ///<inheritdoc/>
+ public int GetSignatureBufferSize() => LibSecp256k1.SignatureSize;
+
+ ///<inheritdoc/>
+ public bool RecoverPublicKey(ReadOnlySpan<byte> privateKey, Span<byte> pubKey)
+ {
+ Check();
+
+ //Init new context
+ using Secp256k1Context context = _lib.CreateContext();
+
+ //Randomize context
+ if (!context.Randomize())
+ {
+ return false;
+ }
+
+ //Recover public key from the privatkey
+ return context.GeneratePubKeyFromSecret(privateKey, pubKey) == LibSecp256k1.XOnlyPublicKeySize;
+ }
+
+ ///<inheritdoc/>
+ public ERRNO SignMessage(ReadOnlySpan<byte> key, ReadOnlySpan<byte> digest, Span<byte> signatureBuffer)
+ {
+ Check();
+
+ //Init new context
+ using Secp256k1Context context = _lib.CreateContext();
+
+ //Randomize context
+ if (!context.Randomize())
+ {
+ return false;
+ }
+
+ //Sign the message
+ return context.SchnorSignDigest(key, digest, signatureBuffer);
+ }
+
+ ///<inheritdoc/>
+ public bool TryGenerateKeyPair(Span<byte> publicKey, Span<byte> privateKey)
+ {
+ //Trim buffers to the exact size required to avoid exceptions in the native lib
+ privateKey = privateKey[..LibSecp256k1.SecretKeySize];
+ publicKey = publicKey[..LibSecp256k1.XOnlyPublicKeySize];
+
+ //Create the secret key
+ _lib.CreateSecretKey(privateKey);
+
+ //Create the public key
+ return RecoverPublicKey(privateKey, publicKey);
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ _lib.Dispose();
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/plugins/nvault/src/NostrEntry.cs b/back-end/plugins/nvault/src/NostrEntry.cs
new file mode 100644
index 0000000..bdb78bb
--- /dev/null
+++ b/back-end/plugins/nvault/src/NostrEntry.cs
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading.Routing;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Sql;
+
+using NVault.Plugins.Vault.Model;
+using NVault.Plugins.Vault.Endpoints;
+
+namespace NVault.Plugins.Vault
+{
+ public sealed class NostrEntry : PluginBase
+ {
+ public override string PluginName { get; } = "NVault";
+
+ protected override void OnLoad()
+ {
+ //Load the endpoint
+ this.Route<Endpoint>();
+
+ //Create the database
+ _ = this.ObserveWork(() => this.EnsureDbCreatedAsync<NostrContext>(this), 1800);
+
+ Log.Information("Plugin loaded");
+ }
+
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+
+ }
+
+ protected override void ProcessHostCommand(string cmd)
+ {
+ throw new NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/back-end/plugins/nvault/src/NostrMessageKind.cs b/back-end/plugins/nvault/src/NostrMessageKind.cs
new file mode 100644
index 0000000..2a53928
--- /dev/null
+++ b/back-end/plugins/nvault/src/NostrMessageKind.cs
@@ -0,0 +1,70 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+namespace NVault.Plugins.Vault
+{
+ internal enum NostrMessageKind
+ {
+ SetMetaData,
+ TextNote,
+ RecommendedServer,
+ Contacts,
+ EncryptedMessage,
+ EventDeletion,
+ Reposts,
+ Reaction,
+ BadgeAward,
+
+ ChannelCreation = 40,
+ ChannelMetadata = 41,
+ ChannelMessage = 42,
+ ChannelHideMessage = 43,
+ ChannelMuteUser = 44,
+
+ FileMetadata = 1063,
+
+ Reporting = 1984,
+
+ ZapRequest = 9734,
+ Zap = 9735,
+
+ MuteList = 10000,
+ PinList = 10001,
+ RelayListMetadata = 10002,
+
+ WalletInfo = 13194,
+
+ ClientAuthenticate = 22242,
+
+ WalletRequest = 23194,
+ WalletResponse = 23195,
+
+ NostrConnect = 24133,
+
+ CategorizedPeopleList = 30000,
+ CategorizedBookmarkList = 30001,
+
+ ProfileBadges = 30008,
+ BadgeDefinition = 30009,
+
+ CreateOrUpdateStall = 30017,
+ CreateOrUpdateAProduct = 30018,
+
+ LongFormContent = 30023,
+
+ ApplicationSpecificData = 30078,
+
+ }
+} \ No newline at end of file
diff --git a/back-end/plugins/nvault/src/NostrOpProvider.cs b/back-end/plugins/nvault/src/NostrOpProvider.cs
new file mode 100644
index 0000000..0f6b1ed
--- /dev/null
+++ b/back-end/plugins/nvault/src/NostrOpProvider.cs
@@ -0,0 +1,282 @@
+// Copyright (C) 2023 Vaughn Nugent
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+using System;
+using System.Threading;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Text.Encodings.Web;
+using System.Security.Cryptography;
+
+using NVault.VaultExtensions;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.IO;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading;
+
+using NVault.Plugins.Vault.Model;
+
+namespace NVault.Plugins.Vault
+{
+ internal sealed class NostrOpProvider : INostrOperations
+ {
+ private static JavaScriptEncoder _encoder { get; } = GetJsEncoder();
+
+ readonly IKvVaultStore _vault;
+ readonly INostrKeyEncoder _keyEncoder;
+ readonly INostrCryptoProvider _cryptoProvider;
+
+ public NostrOpProvider(PluginBase plugin)
+ {
+ //Use base64 key encoder
+ _keyEncoder = new Base64KeyEncoder();
+
+ //Setup crypto provider
+ _cryptoProvider = plugin.CreateService<ManagedCryptoprovider>();
+
+ //Get the vault
+ _vault = plugin.CreateService<ManagedVaultClient>();
+ }
+
+ ///<inheritdoc/>
+ public Task<bool> CreateCredentialAsync(VaultUserScope scope, NostrKeyMeta newKey, CancellationToken cancellation)
+ {
+ //Calculate the required buffer size
+ KeyBufferSizes bufSizes = _cryptoProvider.GetKeyBufferSize();
+
+ //Alloc the buffer
+ using IMemoryHandle<byte> buffHandle = MemoryUtil.SafeAllocNearestPage(bufSizes.PublicKeySize + bufSizes.PrivateKeySize, true);
+
+ //Breakup buffers
+ Span<byte> privKey = buffHandle.Span[..bufSizes.PrivateKeySize];
+ Span<byte> pubKey = buffHandle.Span[bufSizes.PrivateKeySize..];
+
+ //Generate the keypair
+ bool err = _cryptoProvider.TryGenerateKeyPair(pubKey, privKey);
+
+ if (!err)
+ {
+ return Task.FromResult(false);
+ }
+
+ //Trim the buffers
+ privKey = privKey[..bufSizes.PrivateKeySize];
+ pubKey = pubKey[..bufSizes.PublicKeySize];
+
+ //Encode the private key
+ PrivateString? privateKey = (PrivateString?)_keyEncoder.EncodeKey(privKey);
+
+ //Public key is hexadecimal (lowercase)
+ newKey.Value = Convert.ToHexString(pubKey).ToLower();
+
+ //Zero the buffers
+ MemoryUtil.InitializeBlock(buffHandle.Span);
+
+ if (privateKey == null)
+ {
+ return Task.FromResult(false);
+ }
+
+ //Store the keypair in the vault
+ return StorePivKeyAsync(scope, newKey, privateKey, cancellation);
+ }
+
+ ///<inheritdoc/
+ public Task<bool> CreateFromExistingAsync(VaultUserScope scope, NostrKeyMeta newKey, string hexKey, CancellationToken cancellation)
+ {
+ //Calculate the required buffer size
+ KeyBufferSizes bufSizes = _cryptoProvider.GetKeyBufferSize();
+
+ Span<byte> pubkeyBuffer = stackalloc byte[bufSizes.PublicKeySize];
+
+ //Recover the private key from the hex string
+ byte[] privKeyBuffer = Convert.FromHexString(hexKey);
+
+ try
+ {
+ //Recover keypair from private key
+ if (!_cryptoProvider.RecoverPublicKey(privKeyBuffer, pubkeyBuffer))
+ {
+ return Task.FromResult(false);
+ }
+
+ //Encode the private key
+ PrivateString? privateKey = (PrivateString?)_keyEncoder.EncodeKey(privKeyBuffer);
+
+ if (privateKey == null)
+ {
+ return Task.FromResult(false);
+ }
+
+ //Public key is hexadecimal (lowercase)
+ newKey.Value = Convert.ToHexString(pubkeyBuffer).ToLower();
+
+ //Store the keypair in the vault
+ return StorePivKeyAsync(scope, newKey, privateKey, cancellation);
+ }
+ finally
+ {
+ //Always zero the private key buffer
+ MemoryUtil.InitializeBlock(privKeyBuffer.AsSpan());
+ }
+ }
+
+ private async Task<bool> StorePivKeyAsync(VaultUserScope scope, NostrKeyMeta meta, PrivateString key, CancellationToken cancellation)
+ {
+ //Store the key in the vault
+ await _vault.SetSecretAsync(scope, meta.Id, key, cancellation);
+
+ //Erase the key
+ key.Erase();
+
+ return true;
+ }
+
+ ///<inheritdoc/>
+ public Task DeleteCredentialAsync(VaultUserScope scope, NostrKeyMeta key, CancellationToken cancellation) => _vault.DeleteSecretAsync(scope, key.Id, cancellation);
+
+ ///<inheritdoc/>
+ public async Task<bool> SignEventAsync(VaultUserScope scope, NostrKeyMeta keyMeta, NostrEvent evnt, CancellationToken cancellation)
+ {
+ //Get key data from the vault
+ PrivateString? secret = await _vault.GetSecretAsync(scope, keyMeta.Id, cancellation);
+
+ return secret != null && SignMessage(secret, evnt);
+ }
+
+ private bool SignMessage(PrivateString vaultKey, NostrEvent ev)
+ {
+ //Decode the key
+ int keyBufSize = _keyEncoder.GetKeyBufferSize(vaultKey);
+
+ //Get the signature buffer size
+ int sigBufSize = _cryptoProvider.GetSignatureBufferSize();
+
+ //Alloc key buffer
+ using IMemoryHandle<byte> buffHandle = MemoryUtil.SafeAllocNearestPage(keyBufSize + sigBufSize, true);
+
+ //Wrap the buffer
+ EvBuffer buffer = new(buffHandle, keyBufSize, sigBufSize, (int)HashAlg.SHA256);
+
+ try
+ {
+ //Decode the key
+ ERRNO keySize = _keyEncoder.DecodeKey(vaultKey, buffer.KeyBuffer);
+
+ if (!keySize)
+ {
+ return false;
+ }
+
+ //Get the event id/event digest from the event
+ GetNostrEventId(ev, buffer.HashBuffer);
+
+ //Store the event id
+ ev.Id = Convert.ToHexString(buffer.HashBuffer).ToLower();
+
+ //Sign the event
+ ERRNO sigSize = _cryptoProvider.SignMessage(buffer.KeyBuffer[..(int)keySize], buffer.HashBuffer, buffer.SigBuffer);
+
+ if (!sigSize)
+ {
+ return false;
+ }
+
+ //Store the signature as loewrcase hex
+ ev.Signature = Convert.ToHexString(buffer.SigBuffer[..(int)sigSize]).ToLower();
+ return true;
+ }
+ finally
+ {
+ //Zero the key buffer and key
+ MemoryUtil.InitializeBlock(buffHandle.Span);
+ vaultKey.Erase();
+ }
+ }
+
+ private void GetNostrEventId(NostrEvent evnt, Span<byte> idHash)
+ {
+
+ JsonWriterOptions options = new()
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ Indented = false,
+ MaxDepth = 4
+ };
+
+ using VnMemoryStream ms = new();
+ using (Utf8JsonWriter writer = new(ms, options))
+ {
+ //Start the array
+ writer.WriteStartArray();
+
+ //Write 0 to start the array
+ writer.WriteNumberValue(0);
+
+ //We need the public key to be lower case
+ Span<char> lower = stackalloc char[evnt.PublicKey!.Length];
+ evnt.PublicKey.AsSpan().ToLowerInvariant(lower);
+
+ //Write public key, which is hex encoded
+ writer.WriteStringValue(lower);
+
+ //Created-at time
+ writer.WriteNumberValue(evnt.Timestamp!.Value);
+
+ //Kind (as number)
+ writer.WriteNumberValue((int)evnt.MessageKind!.Value);
+
+ //tags (as array of strings)
+ JsonSerializer.Serialize(writer, evnt.Tags);
+
+ //Content as a string
+ writer.WriteStringValue(evnt.Content);
+
+ //End the array
+ writer.WriteEndArray();
+ }
+
+ string raw = System.Text.Encoding.UTF8.GetString(ms.AsSpan());
+
+ //Compute the hash
+ if (!ManagedHash.ComputeHash(ms.AsSpan(), idHash, HashAlg.SHA256))
+ {
+ throw new CryptographicException("Failed to compute event data hash");
+ }
+ }
+
+ private static JavaScriptEncoder GetJsEncoder()
+ {
+ TextEncoderSettings s = new();
+
+ s.AllowCharacters('+');
+
+ return JavaScriptEncoder.Create(s);
+ }
+
+ readonly record struct EvBuffer(IMemoryHandle<byte> Handle, int KeySize, int SigSize, int HashSize)
+ {
+ public readonly Span<byte> KeyBuffer => Handle.Span[..KeySize];
+
+ public readonly Span<byte> SigBuffer => Handle.AsSpan(KeySize, SigSize);
+
+ public readonly Span<byte> HashBuffer => Handle.AsSpan(KeySize + SigSize, HashSize);
+ }
+ }
+}