aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-01-28 19:56:02 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-01-28 19:56:02 -0500
commit87645bfad3943e1110e4cb2e038124083e8ae793 (patch)
treec327a4437c98d973f45c313cf8259ad75515c4fe /lib
parentc438ee90e3be4e5e01ae3d045d6b841a03bd46eb (diff)
progress update
Diffstat (limited to 'lib')
-rw-r--r--lib/NVault.Crypto.Noscrypt/src/LibNoscrypt.cs107
-rw-r--r--lib/NVault.Crypto.Noscrypt/src/NVault.Crypto.Noscrypt.csproj27
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/ContextExtensions.cs274
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/IRandomSource.cs32
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs284
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj27
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/Secp256HashFuncState.cs56
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs76
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/Secp256k1SecretKey.cs58
-rw-r--r--lib/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs101
-rw-r--r--lib/NVault.VaultExtensions/src/IClientAccessScope.cs48
-rw-r--r--lib/NVault.VaultExtensions/src/IKvVaultStore.cs52
-rw-r--r--lib/NVault.VaultExtensions/src/IVaultClientScope.cs33
-rw-r--r--lib/NVault.VaultExtensions/src/IVaultKvClientScope.cs29
-rw-r--r--lib/NVault.VaultExtensions/src/KvVaultStorage.cs66
-rw-r--r--lib/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj27
-rw-r--r--lib/NVault.VaultExtensions/src/VaultClientExtensions.cs156
-rw-r--r--lib/NVault.VaultExtensions/src/VaultUserScope.cs25
18 files changed, 1478 insertions, 0 deletions
diff --git a/lib/NVault.Crypto.Noscrypt/src/LibNoscrypt.cs b/lib/NVault.Crypto.Noscrypt/src/LibNoscrypt.cs
new file mode 100644
index 0000000..ec0ce2d
--- /dev/null
+++ b/lib/NVault.Crypto.Noscrypt/src/LibNoscrypt.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Native;
+
+using NCResult = System.Int64;
+
+namespace NVault.Crypto.Noscrypt
+{
+ public unsafe sealed class LibNoscrypt(SafeLibraryHandle Library, bool OwnsHandle) : VnDisposeable
+ {
+ //Values that match the noscrypt.h header
+ public const int NC_SEC_KEY_SIZE = 32;
+ public const int NC_SEC_PUBKEY_SIZE = 32;
+ public const int NC_ENCRYPTION_NONCE_SIZE = 32;
+ public const int NC_PUBKEY_SIZE = 32;
+ public const int NC_SIGNATURE_SIZE = 64;
+ public const int NC_CONV_KEY_SIZE = 32;
+ public const int NC_MESSAGE_KEY_SIZE = 32;
+ public const int CTX_ENTROPY_SIZE = 32;
+
+ //STRUCTS MUST MATCH THE NOSCRYPT.H HEADER
+
+ [StructLayout(LayoutKind.Sequential, Size = NC_SEC_KEY_SIZE)]
+ internal struct NCSecretKey
+ {
+ public fixed byte key[NC_SEC_KEY_SIZE];
+ }
+
+ [StructLayout(LayoutKind.Sequential, Size = NC_SEC_PUBKEY_SIZE)]
+ internal struct NCPublicKey
+ {
+ public fixed byte key[NC_SEC_PUBKEY_SIZE];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct NCCryptoData
+ {
+ public fixed byte nonce[NC_ENCRYPTION_NONCE_SIZE];
+ public void* inputData;
+ public void* outputData;
+ public uint dataSize;
+ }
+
+ //FUCNTIONS
+ [SafeMethodName("NCGetContextStructSize")]
+ internal delegate uint NCGetContextStructSizeDelegate();
+
+ [SafeMethodName("NCInitContext")]
+ internal delegate NCResult NCInitContextDelegate(void* ctx , byte* entropy32);
+
+ [SafeMethodName("NCReInitContext")]
+ internal delegate NCResult NCReInitContextDelegate(void* ctx, byte* entropy32);
+
+ [SafeMethodName("NCDestroyContext")]
+ internal delegate NCResult NCDestroyContextDelegate(void* ctx);
+
+ [SafeMethodName("NCGetPublicKey")]
+ internal delegate NCResult NCGetPublicKeyDelegate(void* ctx, NCSecretKey* secKey, NCPublicKey* publicKey);
+
+ [SafeMethodName("NCValidateSecretKey")]
+ internal delegate NCResult NCValidateSecretKeyDelegate(void* ctx, NCSecretKey* secKey);
+
+ [SafeMethodName("NCSignData")]
+ internal delegate NCResult NCSignDataDelegate(void* ctx, NCSecretKey* secKey, byte* random32, byte* data, long dataSize, byte* sig64);
+
+ [SafeMethodName("NCVerifyData")]
+ internal delegate NCResult NCVerifyDataDelegate(void* ctx, NCPublicKey* pubKey, byte* data, long dataSize, byte* sig64);
+
+ [SafeMethodName("NCSignDigest")]
+ internal delegate NCResult NCSignDigestDelegate(void* ctx, NCSecretKey* secKey, byte* random32, byte* digest32, byte* sig64);
+
+ [SafeMethodName("NCVerifyDigest")]
+ internal delegate NCResult NCVerifyDigestDelegate(void* ctx, NCPublicKey* pubKey, byte* digest32, byte* sig64);
+
+
+
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ if (OwnsHandle)
+ {
+ Library.Dispose();
+ }
+ }
+
+ public static LibNoscrypt Load(string path, DllImportSearchPath search)
+ {
+ //Load the native library
+ SafeLibraryHandle handle = SafeLibraryHandle.LoadLibrary(path, search);
+
+ //Create the wrapper
+ return new LibNoscrypt(handle, true);
+ }
+
+ public static LibNoscrypt Load(string path) => Load(path, DllImportSearchPath.SafeDirectories);
+
+
+ }
+}
diff --git a/lib/NVault.Crypto.Noscrypt/src/NVault.Crypto.Noscrypt.csproj b/lib/NVault.Crypto.Noscrypt/src/NVault.Crypto.Noscrypt.csproj
new file mode 100644
index 0000000..2416535
--- /dev/null
+++ b/lib/NVault.Crypto.Noscrypt/src/NVault.Crypto.Noscrypt.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <PackageReadmeFile>README.md</PackageReadmeFile>
+ <RootNamespace>NVault.Crypto.Noscrypt</RootNamespace>
+ <AssemblyName>NVault.Crypto.Noscrypt</AssemblyName>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <Authors>Vaughn Nugent</Authors>
+ <Company>Vaughn Nugent</Company>
+ <Product>NVault.Crypto.Noscrypt</Product>
+ <Description>Provides a managed library for the noscrypt native library, along with other helper types for NVault</Description>
+ <Copyright>Copyright © 2024 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-ci0114" />
+ <PackageReference Include="VNLib.Utils" Version="0.1.0-ci0114" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/NVault.Crypto.Secp256k1/src/ContextExtensions.cs b/lib/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
new file mode 100644
index 0000000..bb014df
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
@@ -0,0 +1,274 @@
+// Copyright (C) 2024 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 System.Runtime.InteropServices;
+
+using VNLib.Hashing;
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+
+using static NVault.Crypto.Secp256k1.LibSecp256k1;
+
+namespace NVault.Crypto.Secp256k1
+{
+ /// <summary>
+ /// The callback function signature required for ECDH hash functions
+ /// </summary>
+ /// <param name="state">The callback state</param>
+ /// <returns>The return value to be passed as a result of the operation</returns>
+ public delegate int Secp256k1EcdhHashFunc(in Secp256HashFuncState state);
+
+ public static unsafe class ContextExtensions
+ {
+ /// <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;
+ }
+
+ //Secret key size must be exactly the size of the secret key struct
+ if(secretKey.Length != sizeof(Secp256k1SecretKey))
+ {
+ return ERRNO.E_FAIL;
+ }
+
+ //Stack allocated keypair
+ KeyPair keyPair = new();
+
+ //Init the secret key struct from key data
+ Secp256k1SecretKey secKeyStruct = MemoryMarshal.Read<Secp256k1SecretKey>(secretKey);
+
+ //Randomize the context and create the keypair
+ if (!context.CreateKeyPair(&keyPair, &secKeyStruct))
+ {
+ 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 = &MemoryMarshal.GetReference(signature),
+ digestPtr = &MemoryMarshal.GetReference(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 != sizeof(Secp256k1SecretKey))
+ {
+ throw new CryptographicException($"Your secret key must be exactly {sizeof(Secp256k1SecretKey)} 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
+ Secp256k1PublicKey xOnlyPubKey = new();
+ Secp256k1SecretKey secKeyStruct = MemoryMarshal.Read<Secp256k1SecretKey>(secretKey);
+ KeyPair keyPair = new();
+
+ try
+ {
+ //Init context and keypair
+ if (!context.CreateKeyPair(&keyPair, &secKeyStruct))
+ {
+ 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 = &MemoryMarshal.GetReference(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;
+ }
+
+ /// <summary>
+ /// Verifies that a given secret key is valid using the current context
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="secretKey">The secret key to verify</param>
+ /// <returns>A boolean value that indicates if the secret key is valid or not</returns>
+ /// <exception cref="CryptographicException"></exception>
+ public static bool VerifySecretKey(this in Secp256k1Context context, ReadOnlySpan<byte> secretKey)
+ {
+ if (secretKey.Length != sizeof(Secp256k1SecretKey))
+ {
+ throw new CryptographicException($"Your secret key must be exactly {sizeof(Secp256k1SecretKey)} bytes long");
+ }
+
+ context.Lib.SafeLibHandle.ThrowIfClosed();
+
+ //Get sec key ref and verify
+ fixed(byte* ptr = &MemoryMarshal.GetReference(secretKey))
+ {
+ return context.Lib._secKeyVerify.Invoke(context.Context, ptr) == 1;
+ }
+ }
+
+
+ [StructLayout(LayoutKind.Sequential)]
+ private readonly ref struct EcdhHashFuncState
+ {
+ public readonly IntPtr HashFunc { get; init; }
+ public readonly IntPtr Opaque { get; init; }
+ public readonly int OutLen { get; init; }
+ }
+
+ /// <summary>
+ /// Verifies that a given secret key is valid using the current context
+ /// </summary>
+ /// <param name="context"></param>
+ /// <param name="secretKey">The secret key to verify</param>
+ /// <returns>A boolean value that indicates if the secret key is valid or not</returns>
+ /// <exception cref="ArgumentException"></exception>
+ public static bool ComputeSharedKey(this in Secp256k1Context context, Span<byte> data, ReadOnlySpan<byte> xOnlyPubKey, ReadOnlySpan<byte> secretKey, Secp256k1EcdhHashFunc callback, IntPtr opaque)
+ {
+ if (secretKey.Length != sizeof(Secp256k1SecretKey))
+ {
+ throw new ArgumentException($"Your secret key buffer must be exactly {sizeof(Secp256k1SecretKey)} bytes long");
+ }
+
+ //Init callback state struct
+ EcdhHashFuncState state = new()
+ {
+ HashFunc = Marshal.GetFunctionPointerForDelegate(callback),
+ Opaque = opaque,
+ OutLen = data.Length
+ };
+
+ context.Lib.SafeLibHandle.ThrowIfClosed();
+
+ //Stack allocated keypair and x-only public key
+ Secp256k1PublicKey peerPubKey = new();
+
+ //Parse the public key from the buffer
+ fixed (byte* pubkeyPtr = &MemoryMarshal.GetReference(xOnlyPubKey))
+ {
+ context.Lib._xOnlyPubkeyParse(context.Context, &peerPubKey, pubkeyPtr);
+ }
+
+ fixed (byte* dataPtr = &MemoryMarshal.GetReference(data),
+ secKeyPtr = &MemoryMarshal.GetReference(secretKey))
+ {
+ return context.Lib._ecdh.Invoke(
+ context.Context,
+ dataPtr,
+ &peerPubKey,
+ secKeyPtr,
+ UmanagedEcdhHashFuncCallback,
+ &state
+ ) == 1;
+ }
+
+ /*
+ * Umanaged wrapper function for invoking the safe user callback
+ * from the unmanaged lib
+ */
+ static int UmanagedEcdhHashFuncCallback(byte* output, byte* x32, byte* y32, void* opaque)
+ {
+ //Recover the callback
+ if (opaque == null)
+ {
+ return 0;
+ }
+
+ EcdhHashFuncState* state = (EcdhHashFuncState*)opaque;
+
+ //Init user-state structure
+ Secp256HashFuncState userState = new(output, state->OutLen, x32, 32, new(opaque));
+
+ //Recover the function pointer
+ Secp256k1EcdhHashFunc callback = Marshal.GetDelegateForFunctionPointer<Secp256k1EcdhHashFunc>(state->HashFunc);
+
+ //Invoke the callback
+ return callback(in userState);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/NVault.Crypto.Secp256k1/src/IRandomSource.cs b/lib/NVault.Crypto.Secp256k1/src/IRandomSource.cs
new file mode 100644
index 0000000..4e1861d
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/IRandomSource.cs
@@ -0,0 +1,32 @@
+// Copyright (C) 2024 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/lib/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs b/lib/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
new file mode 100644
index 0000000..8dda269
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
@@ -0,0 +1,284 @@
+// Copyright (C) 2024 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
+{
+
+ internal unsafe delegate int EcdhHasFunc(byte* output, byte* x32, byte* y32, void* data);
+
+ public unsafe class LibSecp256k1 : VnDisposeable
+ {
+ public const int SignatureSize = 64;
+ public const int RandomBufferSize = 32;
+ public const int XOnlyPublicKeySize = 32;
+
+ public static readonly int SecretKeySize = sizeof(Secp256k1SecretKey);
+
+ /*
+ * Unsafe structures that represent the native keypair and x-only public key
+ * structures. They hold character arrays
+ */
+ [StructLayout(LayoutKind.Sequential, Size = 96)]
+ internal struct KeyPair
+ {
+ public fixed byte data[96];
+ }
+
+ /// <summary>
+ /// 1:1 with the secp256k1_pubkey structure
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential, Size = 64)]
+ internal struct Secp256k1PublicKey
+ {
+ public fixed byte data[64];
+ }
+
+
+ //Native methods
+ [SafeMethodName("secp256k1_context_create")]
+ internal delegate IntPtr ContextCreate(int flags);
+
+ [SafeMethodName("secp256k1_context_destroy")]
+ internal delegate void ContextDestroy(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, Secp256k1PublicKey* pubkey, int pk_parity, KeyPair* keypair);
+
+ [SafeMethodName("secp256k1_xonly_pubkey_serialize")]
+ internal delegate int XOnlyPubkeySerialize(IntPtr ctx, byte* output32, Secp256k1PublicKey* pubkey);
+
+ [SafeMethodName("secp256k1_schnorrsig_sign32")]
+ internal delegate int SignHash(IntPtr ctx, byte* sig64, byte* msg32, KeyPair* keypair, byte* aux_rand32);
+
+ [SafeMethodName("secp256k1_ec_seckey_verify")]
+ internal delegate int SecKeyVerify(IntPtr ctx, in byte* seckey);
+
+ [SafeMethodName("secp256k1_ec_pubkey_serialize")]
+ internal delegate int PubKeySerialize(IntPtr ctx, byte* outPubKey, ulong* outLen, Secp256k1PublicKey* pubKey, uint flags);
+
+ [SafeMethodName("secp256k1_xonly_pubkey_parse")]
+ internal delegate int XOnlyPubkeyParse(IntPtr ctx, Secp256k1PublicKey* pubkey, byte* input32);
+
+ [SafeMethodName("secp256k1_ecdh")]
+ internal delegate int Ecdh(
+ IntPtr ctx,
+ byte* output,
+ Secp256k1PublicKey* pubkey,
+ byte* scalar,
+ EcdhHasFunc hashFunc,
+ void* dataPtr
+ );
+
+
+ /// <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>
+ /// Loads the Secp256k1 library from the specified path and creates a wrapper class (loads methods from the library)
+ /// </summary>
+ /// <param name="handle">The handle to the shared library</param>
+ /// <param name="random">An optional random source to create random entropy and secrets from</param>
+ /// <returns>The <see cref="LibSecp256k1"/> library wrapper class</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="MissingMemberException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static LibSecp256k1 FromHandle(SafeLibraryHandle handle, IRandomSource? random)
+ {
+ _ = handle ?? throw new ArgumentNullException(nameof(handle));
+ //setup fallback random source if null
+ random ??= new FallbackRandom();
+ //Create the lib
+ return new LibSecp256k1(handle, random);
+ }
+
+ /// <summary>
+ /// The underlying library handle
+ /// </summary>
+ public SafeLibraryHandle SafeLibHandle { get; }
+
+ internal readonly KeypairCreate _createKeyPair;
+ internal readonly ContextCreate _create;
+ internal readonly RandomizeContext _randomize;
+ internal readonly ContextDestroy _destroy;
+ internal readonly KeypairXOnlyPub _createXonly;
+ internal readonly XOnlyPubkeySerialize _serializeXonly;
+ internal readonly SignHash _signHash;
+ internal readonly SecKeyVerify _secKeyVerify;
+ internal readonly PubKeySerialize _pubKeySerialize;
+ internal readonly Ecdh _ecdh;
+ internal readonly XOnlyPubkeyParse _xOnlyPubkeyParse;
+ 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<ContextCreate>();
+ _createKeyPair = handle.DangerousGetMethod<KeypairCreate>();
+ _randomize = handle.DangerousGetMethod<RandomizeContext>();
+ _destroy = handle.DangerousGetMethod<ContextDestroy>();
+ _createXonly = handle.DangerousGetMethod<KeypairXOnlyPub>();
+ _serializeXonly = handle.DangerousGetMethod<XOnlyPubkeySerialize>();
+ _signHash = handle.DangerousGetMethod<SignHash>();
+ _secKeyVerify = handle.DangerousGetMethod<SecKeyVerify>();
+ _pubKeySerialize = handle.DangerousGetMethod<PubKeySerialize>();
+ _ecdh = handle.DangerousGetMethod<Ecdh>();
+ _xOnlyPubkeyParse = handle.DangerousGetMethod<XOnlyPubkeyParse>();
+
+ //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
+ /// <para>
+ /// NOTE: You should verify this validity of the key against the library with a new <see cref="Secp256k1Context"/>
+ /// </para>
+ /// </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 != sizeof(Secp256k1SecretKey))
+ {
+ throw new ArgumentException($"Buffer must be exactly {sizeof(Secp256k1SecretKey)} 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);
+ }
+
+ /// <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="OutOfMemoryException"></exception>
+ public Secp256k1Context CreateContext()
+ {
+ //Protect for released lib
+ SafeLibHandle.ThrowIfClosed();
+
+ //Create new context
+ IntPtr context = _create(1);
+
+ if (context == IntPtr.Zero)
+ {
+ throw new OutOfMemoryException("Failed to create the new Secp256k1 context");
+ }
+
+ return new Secp256k1Context(this, context);
+ }
+
+ 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/lib/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj b/lib/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj
new file mode 100644
index 0000000..efd78ab
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net8.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 © 2024 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-ci0114" />
+ <PackageReference Include="VNLib.Utils" Version="0.1.0-ci0114" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/NVault.Crypto.Secp256k1/src/Secp256HashFuncState.cs b/lib/NVault.Crypto.Secp256k1/src/Secp256HashFuncState.cs
new file mode 100644
index 0000000..c82321c
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/Secp256HashFuncState.cs
@@ -0,0 +1,56 @@
+// Copyright (C) 2024 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;
+
+namespace NVault.Crypto.Secp256k1
+{
+ [StructLayout(LayoutKind.Sequential)]
+ public unsafe readonly ref struct Secp256HashFuncState
+ {
+
+ /// <summary>
+ /// The opaque pointer passed to the hash function
+ /// </summary>
+ public readonly IntPtr Opaque { get; }
+
+ private readonly byte* _output;
+ private readonly byte* _xCoord;
+ private readonly int _outputLength;
+ private readonly int _xCoordLength;
+
+ internal Secp256HashFuncState(byte* output, int outputLength, byte* xCoord, int xCoordLength, IntPtr opaque)
+ {
+ Opaque = opaque;
+ _output = output;
+ _outputLength = outputLength;
+ _xCoord = xCoord;
+ _xCoordLength = xCoordLength;
+ }
+
+ /// <summary>
+ /// Gets the output buffer as a span
+ /// </summary>
+ /// <returns>The output buffer span</returns>
+ public readonly Span<byte> GetOutput() => new(_output, _outputLength);
+
+ /// <summary>
+ /// Gets the x coordinate argument as a span
+ /// </summary>
+ /// <returns>The xcoordinate buffer span</returns>
+ public readonly ReadOnlySpan<byte> GetXCoordArg() => new(_xCoord, _xCoordLength);
+ }
+} \ No newline at end of file
diff --git a/lib/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs b/lib/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs
new file mode 100644
index 0000000..dfb3ff8
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/Secp256k1Context.cs
@@ -0,0 +1,76 @@
+// Copyright (C) 2024 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, Secp256k1SecretKey* secretKey)
+ {
+ Lib.SafeLibHandle.ThrowIfClosed();
+
+ //Create the keypair from the secret key
+ return Lib._createKeyPair(Context, keyPair, (byte*)secretKey) == 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/lib/NVault.Crypto.Secp256k1/src/Secp256k1SecretKey.cs b/lib/NVault.Crypto.Secp256k1/src/Secp256k1SecretKey.cs
new file mode 100644
index 0000000..35734ae
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/Secp256k1SecretKey.cs
@@ -0,0 +1,58 @@
+// Copyright (C) 2024 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.Memory;
+
+namespace NVault.Crypto.Secp256k1
+{
+ /// <summary>
+ /// Represents a Secp256k1 secret key, the size is fixed, and should use
+ /// the sizeof() operator to get the size
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential, Size = 32)]
+ public unsafe struct Secp256k1SecretKey
+ {
+ private fixed byte data[32];
+
+ /// <summary>
+ /// Implict cast to a span of raw bytes
+ /// </summary>
+ /// <param name="key">The secret key to cast</param>
+ public static implicit operator Span<byte>(Secp256k1SecretKey key) => new(key.data, 32);
+
+ /// <summary>
+ /// Casts the secret key span to a <see cref="Secp256k1SecretKey"/> via a structure copy
+ /// </summary>
+ /// <param name="key">The key data to copy</param>
+ /// <exception cref="ArgumentOutOfRangeException"></exception>
+ public static explicit operator Secp256k1SecretKey(ReadOnlySpan<byte> key) => FromSpan(key);
+
+ /// <summary>
+ /// Creates a new <see cref="Secp256k1SecretKey"/> from a span of bytes
+ /// by copying the bytes into the struct
+ /// </summary>
+ /// <param name="span">The secret key data to copy</param>
+ /// <returns>An initilaized <see cref="Secp256k1SecretKey"/></returns>
+ public static Secp256k1SecretKey FromSpan(ReadOnlySpan<byte> span)
+ {
+ Secp256k1SecretKey newKey = new();
+ MemoryUtil.CopyStruct(span, ref newKey);
+ return newKey;
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs b/lib/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs
new file mode 100644
index 0000000..360de21
--- /dev/null
+++ b/lib/NVault.Crypto.Secp256k1/src/UnmanagedRandomSource.cs
@@ -0,0 +1,101 @@
+// Copyright (C) 2024 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 = &MemoryMarshal.GetReference(buffer))
+ {
+ _getRandomBytes(ptr, buffer.Length);
+ }
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ if (OwnsHandle)
+ {
+ _library.Dispose();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/NVault.VaultExtensions/src/IClientAccessScope.cs b/lib/NVault.VaultExtensions/src/IClientAccessScope.cs
new file mode 100644
index 0000000..c79f75e
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/IClientAccessScope.cs
@@ -0,0 +1,48 @@
+// Copyright (C) 2024 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/lib/NVault.VaultExtensions/src/IKvVaultStore.cs b/lib/NVault.VaultExtensions/src/IKvVaultStore.cs
new file mode 100644
index 0000000..037fe6c
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/IKvVaultStore.cs
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 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/lib/NVault.VaultExtensions/src/IVaultClientScope.cs b/lib/NVault.VaultExtensions/src/IVaultClientScope.cs
new file mode 100644
index 0000000..d53bc4a
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/IVaultClientScope.cs
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 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/lib/NVault.VaultExtensions/src/IVaultKvClientScope.cs b/lib/NVault.VaultExtensions/src/IVaultKvClientScope.cs
new file mode 100644
index 0000000..f763473
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/IVaultKvClientScope.cs
@@ -0,0 +1,29 @@
+// Copyright (C) 2024 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/lib/NVault.VaultExtensions/src/KvVaultStorage.cs b/lib/NVault.VaultExtensions/src/KvVaultStorage.cs
new file mode 100644
index 0000000..8a2b9b6
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/KvVaultStorage.cs
@@ -0,0 +1,66 @@
+// Copyright (C) 2024 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/lib/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj b/lib/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
new file mode 100644
index 0000000..e5dbe8c
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net8.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 © 2024 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-ci0047" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/NVault.VaultExtensions/src/VaultClientExtensions.cs b/lib/NVault.VaultExtensions/src/VaultClientExtensions.cs
new file mode 100644
index 0000000..d90941a
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/VaultClientExtensions.cs
@@ -0,0 +1,156 @@
+// Copyright (C) 2024 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 : PrivateString.ToPrivateString(value, true);
+ }
+
+ /// <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/lib/NVault.VaultExtensions/src/VaultUserScope.cs b/lib/NVault.VaultExtensions/src/VaultUserScope.cs
new file mode 100644
index 0000000..0e8796c
--- /dev/null
+++ b/lib/NVault.VaultExtensions/src/VaultUserScope.cs
@@ -0,0 +1,25 @@
+// Copyright (C) 2024 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