aboutsummaryrefslogtreecommitdiff
path: root/back-end
diff options
context:
space:
mode:
Diffstat (limited to 'back-end')
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs110
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs45
-rw-r--r--back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj4
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj2
-rw-r--r--back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs2
-rw-r--r--back-end/plugins/nvault/src/EncryptionResult.cs28
-rw-r--r--back-end/plugins/nvault/src/Endpoints/Endpoint.cs172
-rw-r--r--back-end/plugins/nvault/src/INostrCryptoProvider.cs10
-rw-r--r--back-end/plugins/nvault/src/INostrOperations.cs5
-rw-r--r--back-end/plugins/nvault/src/ManagedCryptoprovider.cs20
-rw-r--r--back-end/plugins/nvault/src/NVault.csproj7
-rw-r--r--back-end/plugins/nvault/src/NativeSecp256k1Library.cs92
-rw-r--r--back-end/plugins/nvault/src/NostrOpProvider.cs178
13 files changed, 646 insertions, 29 deletions
diff --git a/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
index a89fce8..f5327df 100644
--- a/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/ContextExtensions.cs
@@ -24,9 +24,46 @@ using VNLib.Utils.Extensions;
using static NVault.Crypto.Secp256k1.LibSecp256k1;
-
namespace NVault.Crypto.Secp256k1
{
+ public delegate int Secp256k1EcdhHashFunc(in Secp256HashFuncState state);
+
+ [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);
+ }
+
public static unsafe class ContextExtensions
{
/// <summary>
@@ -138,7 +175,7 @@ namespace NVault.Crypto.Secp256k1
context.Lib.SafeLibHandle.ThrowIfClosed();
//Stack allocated keypair and x-only public key
- XOnlyPubKey xOnlyPubKey = new();
+ Secp256k1PublicKey xOnlyPubKey = new();
KeyPair keyPair = new();
try
@@ -196,5 +233,74 @@ namespace NVault.Crypto.Secp256k1
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 != SecretKeySize)
+ {
+ throw new ArgumentException($"Your public key buffer must be exactly {SecretKeySize} bytes long");
+ }
+
+ //Init callback state struct
+ EcdhHashFuncState state = new()
+ {
+ HashFunc = Marshal.GetFunctionPointerForDelegate(callback),
+ Opaque = opaque,
+ OutLen = data.Length
+ };
+
+ //Stack allocated keypair and x-only public key
+ Secp256k1PublicKey pubKeyStruct = new();
+ //Recover the x-only public key structure
+ MemoryUtil.CopyStruct(xOnlyPubKey, &pubKeyStruct);
+
+ context.Lib.SafeLibHandle.ThrowIfClosed();
+
+ fixed (byte* dataPtr = &MemoryMarshal.GetReference(data),
+ secKeyPtr = &MemoryMarshal.GetReference(secretKey))
+ {
+ return context.Lib._ecdh.Invoke(context.Context, dataPtr, &pubKeyStruct, 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/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs b/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
index 80d6eb8..5aeed00 100644
--- a/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/LibSecp256k1.cs
@@ -23,33 +23,37 @@ 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 SecretKeySize = 32;
- public const int XOnlyPublicKeySize = 32;
public const int SignatureSize = 64;
- public const int KeyPairSize = 96;
public const int RandomBufferSize = 32;
+ public const int XOnlyPublicKeySize = 32;
/*
* Unsafe structures that represent the native keypair and x-only public key
* structures. They hold character arrays
*/
- [StructLayout(LayoutKind.Sequential)]
+ [StructLayout(LayoutKind.Sequential, Size = 96)]
internal struct KeyPair
{
- public fixed byte data[Length];
- public const int Length = KeyPairSize;
+ public fixed byte data[96];
}
- [StructLayout(LayoutKind.Sequential)]
- internal struct XOnlyPubKey
+ /// <summary>
+ /// 1:1 with the secp256k1_pubkey structure
+ /// </summary>
+ [StructLayout(LayoutKind.Sequential, Size = 64)]
+ internal struct Secp256k1PublicKey
{
- public fixed byte data[Length];
- public const int Length = 64;
+ public fixed byte data[64];
}
+
+
//Native methods
[SafeMethodName("secp256k1_context_create")]
internal delegate IntPtr CreateContext(int flags);
@@ -64,10 +68,10 @@ namespace NVault.Crypto.Secp256k1
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);
+ 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, XOnlyPubKey* pubkey);
+ 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);
@@ -75,6 +79,21 @@ namespace NVault.Crypto.Secp256k1
[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_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>
@@ -122,6 +141,8 @@ namespace NVault.Crypto.Secp256k1
internal readonly XOnlyPubkeySerialize _serializeXonly;
internal readonly SignHash _signHash;
internal readonly SecKeyVerify _secKeyVerify;
+ internal readonly PubKeySerialize _pubKeySerialize;
+ internal readonly Ecdh _ecdh;
private readonly IRandomSource _randomSource;
/// <summary>
@@ -148,6 +169,8 @@ namespace NVault.Crypto.Secp256k1
_serializeXonly = handle.DangerousGetMethod<XOnlyPubkeySerialize>();
_signHash = handle.DangerousGetMethod<SignHash>();
_secKeyVerify = handle.DangerousGetMethod<SecKeyVerify>();
+ _pubKeySerialize = handle.DangerousGetMethod<PubKeySerialize>();
+ _ecdh = handle.DangerousGetMethod<Ecdh>();
//Store random source
_randomSource = randomSource;
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
index 7fa738d..22a7e38 100644
--- a/back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj
+++ b/back-end/libs/NVault.Crypto.Secp256k1/src/NVault.Crypto.Secp256k1.csproj
@@ -20,8 +20,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="VNLib.Hashing.Portable" Version="0.1.0-ci0088" />
- <PackageReference Include="VNLib.Utils" Version="0.1.0-ci0088" />
+ <PackageReference Include="VNLib.Hashing.Portable" Version="0.1.0-ci0096" />
+ <PackageReference Include="VNLib.Utils" Version="0.1.0-ci0096" />
</ItemGroup>
</Project>
diff --git a/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj b/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
index 49d3dc6..d6af22d 100644
--- a/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
+++ b/back-end/libs/NVault.VaultExtensions/src/NVault.VaultExtensions.csproj
@@ -21,7 +21,7 @@
<ItemGroup>
<PackageReference Include="VaultSharp" Version="1.13.0.1" />
- <PackageReference Include="VNLib.Plugins.Extensions.Loading" Version="0.1.0-ci0037" />
+ <PackageReference Include="VNLib.Plugins.Extensions.Loading" Version="0.1.0-ci0042" />
</ItemGroup>
</Project>
diff --git a/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs b/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs
index 5a7c637..9ea9d24 100644
--- a/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs
+++ b/back-end/libs/NVault.VaultExtensions/src/VaultClientExtensions.cs
@@ -54,7 +54,7 @@ namespace NVault.VaultExtensions
string? value = result.Data.Data.GetValueOrDefault(property)?.ToString();
//Return the secret value as a private string
- return value == null ? null : new PrivateString(value);
+ return value == null ? null : PrivateString.ToPrivateString(value, true);
}
/// <summary>
diff --git a/back-end/plugins/nvault/src/EncryptionResult.cs b/back-end/plugins/nvault/src/EncryptionResult.cs
new file mode 100644
index 0000000..ad08629
--- /dev/null
+++ b/back-end/plugins/nvault/src/EncryptionResult.cs
@@ -0,0 +1,28 @@
+// 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;
+
+namespace NVault.Plugins.Vault
+{
+ internal class EncryptionResult
+ {
+ [JsonPropertyName("ciphertext")]
+ public string? CipherText { get; set; }
+
+ [JsonPropertyName("iv")]
+ public string? Iv { get; set; }
+ }
+}
diff --git a/back-end/plugins/nvault/src/Endpoints/Endpoint.cs b/back-end/plugins/nvault/src/Endpoints/Endpoint.cs
index 3d400ab..f718c2f 100644
--- a/back-end/plugins/nvault/src/Endpoints/Endpoint.cs
+++ b/back-end/plugins/nvault/src/Endpoints/Endpoint.cs
@@ -18,6 +18,8 @@ using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
@@ -25,6 +27,7 @@ using FluentValidation;
using NVault.VaultExtensions;
+using VNLib.Utils.Extensions;
using VNLib.Plugins;
using VNLib.Plugins.Essentials;
using VNLib.Plugins.Essentials.Endpoints;
@@ -36,6 +39,7 @@ using VNLib.Plugins.Extensions.Data.Extensions;
using NVault.Plugins.Vault.Model;
+
namespace NVault.Plugins.Vault.Endpoints
{
@@ -46,6 +50,8 @@ namespace NVault.Plugins.Vault.Endpoints
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 static IValidator<Nip04DecryptRequest> DecrptMessageValidator { get; } = Nip04DecryptRequest.GetValidator();
+ private static IValidator<Nip04EncryptRequest> EncryptMessageValidator { get; } = Nip04EncryptRequest.GetValidator();
private readonly INostrOperations _vault;
private readonly NostrRelayStore _relays;
@@ -106,13 +112,12 @@ namespace NVault.Plugins.Vault.Endpoints
}
protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
- {
+ {
+ ValErrWebMessage webm = new();
//Get the operation argument
- if(entity.QueryArgs.IsArgumentSet("type", "signEvent"))
+ if (entity.QueryArgs.IsArgumentSet("type", "signEvent"))
{
- ValErrWebMessage webm = new();
-
//Get the event
NostrEvent? nEvent = await entity.GetJsonFromFileAsync<NostrEvent>();
@@ -164,6 +169,92 @@ namespace NVault.Plugins.Vault.Endpoints
return VirtualOk(entity, webm);
}
+ //Decryption
+ if (entity.QueryArgs.IsArgumentSet("type", "decrypt"))
+ {
+ //Recover the decryption request
+ Nip04DecryptRequest? request = await entity.GetJsonFromFileAsync<Nip04DecryptRequest>();
+
+ if (webm.Assert(request != null, "No decryption request received"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.BadRequest);
+ }
+
+ if (!DecrptMessageValidator.Validate(request, webm))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity);
+ }
+
+ //Recover the current users key metadata
+ NostrKeyMeta? key = await _publicKeyStore.GetSingleUserRecordAsync(request.KeyId!, entity.Session.UserID);
+
+ if (webm.Assert(key != null, "Key metadata not found"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.NotFound);
+ }
+
+ VaultUserScope scope = new(entity.Session.UserID);
+
+ //Try to decrypt the message
+ webm.Result = await _vault.DecryptNoteAsync(
+ scope,
+ key,
+ request.OtherPubKey!,
+ request.Ciphertext!,
+ entity.EventCancellation
+ );
+
+ webm.Success = true;
+
+ return VirtualOk(entity, webm);
+ }
+
+ //Encryption
+ if (entity.QueryArgs.IsArgumentSet("type", "encrypt"))
+ {
+ //Recover the decryption request
+ Nip04EncryptRequest? request = await entity.GetJsonFromFileAsync<Nip04EncryptRequest>();
+
+ if (webm.Assert(request != null, "No decryption request received"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.BadRequest);
+ }
+
+ if (!EncryptMessageValidator.Validate(request, webm))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity);
+ }
+
+ //Recover the current user's key metadata
+ NostrKeyMeta? key = await _publicKeyStore.GetSingleUserRecordAsync(request.KeyId!, entity.Session.UserID);
+
+ if (webm.Assert(key != null, "Key metadata not found"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.NotFound);
+ }
+
+ VaultUserScope scope = new(entity.Session.UserID);
+ try
+ {
+ //Try to encrypt the message
+ webm.Result = await _vault.EncryptNoteAsync(
+ scope,
+ key,
+ request.OtherPubKey!,
+ request.PlainText!,
+ entity.EventCancellation
+ );
+
+ webm.Success = true;
+ }
+ catch (CryptographicException)
+ {
+ webm.Result = "Failed to encrypt the ciphertext";
+ }
+
+ return VirtualOk(entity, webm);
+ }
+
return VfReturnType.NotFound;
}
@@ -400,5 +491,78 @@ namespace NVault.Plugins.Vault.Endpoints
return val;
}
}
+
+
+ sealed class Nip04DecryptRequest
+ {
+ [JsonPropertyName("KeyId")]
+ public string? KeyId { get; set; }
+
+ [JsonPropertyName("content")]
+ public string? Ciphertext { get; set; }
+
+ [JsonPropertyName("pubkey")]
+ public string? OtherPubKey { get; set; }
+
+ public static IValidator<Nip04DecryptRequest> GetValidator()
+ {
+ InlineValidator<Nip04DecryptRequest> validationRules = new();
+
+ validationRules.RuleFor(p => p.KeyId)
+ .NotEmpty()!
+ .AlphaNumericOnly()
+ .Length(1, 100);
+
+ validationRules.RuleFor(p => p.Ciphertext)
+ .NotEmpty()
+ .Length(0, 10000)
+ //Make sure iv exists
+ .Must(ct => ct.Contains("iv?=", StringComparison.OrdinalIgnoreCase))
+ //Check iv is not too long
+ .Must(ct => ct.AsSpan().SliceAfterParam("iv?=").Length < 28);
+
+ //Pubpkey must be 64 hex characters
+ validationRules.RuleFor(p => p.OtherPubKey)
+ .NotEmpty()
+ .Length(64)
+ .AlphaNumericOnly();
+
+ return validationRules;
+ }
+ }
+
+ sealed class Nip04EncryptRequest
+ {
+ [JsonPropertyName("KeyId")]
+ public string? KeyId { get; set; }
+
+ [JsonPropertyName("content")]
+ public string? PlainText { get; set; }
+
+ [JsonPropertyName("pubkey")]
+ public string? OtherPubKey { get; set; }
+
+ public static IValidator<Nip04EncryptRequest> GetValidator()
+ {
+ InlineValidator<Nip04EncryptRequest> validationRules = new();
+
+ validationRules.RuleFor(p => p.KeyId)
+ .NotEmpty()!
+ .AlphaNumericOnly()
+ .Length(1, 100);
+
+ validationRules.RuleFor(p => p.PlainText)
+ .NotEmpty()
+ .Length(0, 10000);
+
+ //Pubpkey must be 64 hex characters
+ validationRules.RuleFor(p => p.OtherPubKey)
+ .NotEmpty()
+ .Length(64)
+ .AlphaNumericOnly();
+
+ return validationRules;
+ }
+ }
}
}
diff --git a/back-end/plugins/nvault/src/INostrCryptoProvider.cs b/back-end/plugins/nvault/src/INostrCryptoProvider.cs
index 805eb21..d6c1e8a 100644
--- a/back-end/plugins/nvault/src/INostrCryptoProvider.cs
+++ b/back-end/plugins/nvault/src/INostrCryptoProvider.cs
@@ -60,6 +60,16 @@ namespace NVault.Plugins.Vault
/// <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);
+
+ ERRNO DecryptMessage(ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> targetKey, ReadOnlySpan<byte> aseIv, ReadOnlySpan<byte> cyphterText, Span<byte> outputBuffer);
+
+ ERRNO EncryptMessage(ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> targetKey, ReadOnlySpan<byte> aesIv, ReadOnlySpan<byte> plainText, Span<byte> cipherText);
+
+ /// <summary>
+ /// Fill a buffer with secure randomness/entropy
+ /// </summary>
+ /// <param name="bytes">A span of memory to fill with random data</param>
+ void GetRandomBytes(Span<byte> bytes);
}
readonly record struct KeyBufferSizes(int PrivateKeySize, int PublicKeySize);
diff --git a/back-end/plugins/nvault/src/INostrOperations.cs b/back-end/plugins/nvault/src/INostrOperations.cs
index b7d82a2..efb5947 100644
--- a/back-end/plugins/nvault/src/INostrOperations.cs
+++ b/back-end/plugins/nvault/src/INostrOperations.cs
@@ -22,6 +22,7 @@ using NVault.VaultExtensions;
namespace NVault.Plugins.Vault
{
+
internal interface INostrOperations
{
Task<bool> SignEventAsync(VaultUserScope scope, NostrKeyMeta keyMeta, NostrEvent evnt, CancellationToken cancellation);
@@ -31,5 +32,9 @@ namespace NVault.Plugins.Vault
Task<bool> CreateFromExistingAsync(VaultUserScope scope, NostrKeyMeta newKey, string hexKey, CancellationToken cancellation);
Task DeleteCredentialAsync(VaultUserScope scope, NostrKeyMeta key, CancellationToken cancellation);
+
+ Task<string?> DecryptNoteAsync(VaultUserScope scope, NostrKeyMeta key, string targetPubKeyHex, string nip04Ciphertext, CancellationToken cancellation);
+
+ Task<EncryptionResult> EncryptNoteAsync(VaultUserScope scope, NostrKeyMeta meta, string targetPubKey, string plainText, CancellationToken cancellation);
}
}
diff --git a/back-end/plugins/nvault/src/ManagedCryptoprovider.cs b/back-end/plugins/nvault/src/ManagedCryptoprovider.cs
index a985ba3..2eeae56 100644
--- a/back-end/plugins/nvault/src/ManagedCryptoprovider.cs
+++ b/back-end/plugins/nvault/src/ManagedCryptoprovider.cs
@@ -44,7 +44,7 @@ namespace NVault.Plugins.Vault
if (isManaged)
{
//Load managed assembly, plugin will manage lifetime
- random = plugin.LoadAssembly<IRandomSource>(path).Resource;
+ random = plugin.CreateServiceExternal<IRandomSource>(path);
}
else
{
@@ -77,5 +77,23 @@ namespace NVault.Plugins.Vault
///<inheritdoc/>
public bool RecoverPublicKey(ReadOnlySpan<byte> privateKey, Span<byte> pubKey) => _provider.RecoverPublicKey(privateKey, pubKey);
+
+ ///<inheritdoc/>
+ public ERRNO DecryptMessage(ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> targetKey, ReadOnlySpan<byte> aseIv, ReadOnlySpan<byte> cyphterText, Span<byte> outputBuffer)
+ {
+ return _provider.DecryptMessage(secretKey, targetKey, aseIv, cyphterText, outputBuffer);
+ }
+
+ ///<inheritdoc/>
+ public ERRNO EncryptMessage(ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> targetKey, ReadOnlySpan<byte> aesIv, ReadOnlySpan<byte> plainText, Span<byte> cipherText)
+ {
+ return _provider.EncryptMessage(secretKey, targetKey, aesIv, plainText, cipherText);
+ }
+
+ ///<inheritdoc/>
+ public void GetRandomBytes(Span<byte> bytes)
+ {
+ _provider.GetRandomBytes(bytes);
+ }
}
}
diff --git a/back-end/plugins/nvault/src/NVault.csproj b/back-end/plugins/nvault/src/NVault.csproj
index 7f426b9..28ad3ee 100644
--- a/back-end/plugins/nvault/src/NVault.csproj
+++ b/back-end/plugins/nvault/src/NVault.csproj
@@ -21,8 +21,8 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.8.0" />
- <PackageReference Include="VNLib.Plugins.Extensions.Data" Version="0.1.0-ci0037" />
- <PackageReference Include="VNLib.Plugins.Extensions.Validation" Version="0.1.0-ci0037" />
+ <PackageReference Include="VNLib.Plugins.Extensions.Data" Version="0.1.0-ci0042" />
+ <PackageReference Include="VNLib.Plugins.Extensions.Validation" Version="0.1.0-ci0042" />
<PackageReference Include="VVNLib.Plugins.Extensions.Loading.Sql" Version="0.1.0-ci0034" />
</ItemGroup>
@@ -31,9 +31,10 @@
<ProjectReference Include="..\..\..\libs\NVault.VaultExtensions\src\NVault.VaultExtensions.csproj" />
</ItemGroup>
<ItemGroup>
- <None Update="example.NVault.json">
+ <None Update="NVault.example.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
+
</Project>
diff --git a/back-end/plugins/nvault/src/NativeSecp256k1Library.cs b/back-end/plugins/nvault/src/NativeSecp256k1Library.cs
index 6c670dd..55cf2de 100644
--- a/back-end/plugins/nvault/src/NativeSecp256k1Library.cs
+++ b/back-end/plugins/nvault/src/NativeSecp256k1Library.cs
@@ -14,10 +14,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using System;
+using System.Security.Cryptography;
+using System.Runtime.InteropServices;
using NVault.Crypto.Secp256k1;
using VNLib.Utils;
+using VNLib.Utils.Memory;
namespace NVault.Plugins.Vault
{
@@ -38,10 +41,92 @@ namespace NVault.Plugins.Vault
/// <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);
+ LibSecp256k1 lib = LibSecp256k1.LoadLibrary(libFilePath, DllImportSearchPath.SafeDirectories, random);
return new(lib);
}
+ ///<inheritdoc/>
+ public ERRNO DecryptMessage(ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> targetKey, ReadOnlySpan<byte> aesIv, ReadOnlySpan<byte> ciphterText, Span<byte> outputBuffer)
+ {
+ Check();
+ //Start with new context
+ using Secp256k1Context context = _lib.CreateContext();
+
+ //Randomize context
+ if (!context.Randomize())
+ {
+ return false;
+ }
+
+ //Get shared key
+ byte[] sharedKeyBuffer = new byte[32];
+
+ try
+ {
+ //Get the Secp256k1 shared key
+ context.ComputeSharedKey(sharedKeyBuffer, targetKey, secretKey, HashFuncCallback, IntPtr.Zero);
+
+ //Init the AES cipher
+ using Aes aes = Aes.Create();
+ aes.Key = sharedKeyBuffer;
+ aes.Mode = CipherMode.CBC;
+
+ return aes.DecryptCbc(ciphterText, aesIv, outputBuffer, PaddingMode.None);
+ }
+ finally
+ {
+ //Zero out buffers
+ MemoryUtil.InitializeBlock(sharedKeyBuffer.AsSpan());
+ }
+ }
+
+ ///<inheritdoc/>
+ public ERRNO EncryptMessage(ReadOnlySpan<byte> secretKey, ReadOnlySpan<byte> targetKey, ReadOnlySpan<byte> aesIv, ReadOnlySpan<byte> plainText, Span<byte> cipherText)
+ {
+ Check();
+ //Start with new context
+ using Secp256k1Context context = _lib.CreateContext();
+
+ //Randomize context
+ if (!context.Randomize())
+ {
+ return false;
+ }
+
+ //Get shared key
+ byte[] sharedKeyBuffer = new byte[32];
+
+ try
+ {
+ //Get the Secp256k1 shared key
+ context.ComputeSharedKey(sharedKeyBuffer, targetKey, secretKey, HashFuncCallback, IntPtr.Zero);
+
+ //Init the AES cipher
+ using Aes aes = Aes.Create();
+ aes.Key = sharedKeyBuffer;
+ aes.Mode = CipherMode.CBC;
+
+ return aes.EncryptCbc(plainText, aesIv, cipherText, PaddingMode.None);
+ }
+ finally
+ {
+ //Zero out buffers
+ MemoryUtil.InitializeBlock(sharedKeyBuffer.AsSpan());
+ }
+ }
+
+ static int HashFuncCallback(in Secp256HashFuncState state)
+ {
+ //Get function args
+ Span<byte> sharedKey = state.GetOutput();
+ ReadOnlySpan<byte> xCoord = state.GetXCoordArg();
+
+ //Nostr literally just uses the shared x coord as the shared key
+ xCoord.CopyTo(sharedKey);
+
+ return xCoord.Length;
+ }
+
//Key sizes are constant
///<inheritdoc/>
public KeyBufferSizes GetKeyBufferSize() => new(LibSecp256k1.SecretKeySize, LibSecp256k1.XOnlyPublicKeySize);
@@ -87,6 +172,9 @@ namespace NVault.Plugins.Vault
}
///<inheritdoc/>
+ public void GetRandomBytes(Span<byte> bytes) => _lib.GetRandomBytes(bytes);
+
+ ///<inheritdoc/>
public bool TryGenerateKeyPair(Span<byte> publicKey, Span<byte> privateKey)
{
//Trim buffers to the exact size required to avoid exceptions in the native lib
@@ -120,5 +208,7 @@ namespace NVault.Plugins.Vault
{
_lib.Dispose();
}
+
+
}
} \ No newline at end of file
diff --git a/back-end/plugins/nvault/src/NostrOpProvider.cs b/back-end/plugins/nvault/src/NostrOpProvider.cs
index d2374f9..eed64b7 100644
--- a/back-end/plugins/nvault/src/NostrOpProvider.cs
+++ b/back-end/plugins/nvault/src/NostrOpProvider.cs
@@ -14,8 +14,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using System;
+using System.Text;
using System.Threading;
using System.Text.Json;
+using System.Buffers.Text;
using System.Threading.Tasks;
using System.Text.Encodings.Web;
using System.Security.Cryptography;
@@ -32,10 +34,13 @@ using VNLib.Plugins.Extensions.Loading;
using NVault.Plugins.Vault.Model;
+
namespace NVault.Plugins.Vault
{
internal sealed class NostrOpProvider : INostrOperations
{
+ const int NIP04_RANDOM_IV_SIZE = 16;
+
private static JavaScriptEncoder _encoder { get; } = GetJsEncoder();
readonly IKvVaultStore _vault;
@@ -155,12 +160,12 @@ namespace NVault.Plugins.Vault
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);
+ using PrivateString? secret = await _vault.GetSecretAsync(scope, keyMeta.Id, cancellation);
return secret != null && SignMessage(secret, evnt);
}
- private bool SignMessage(PrivateString vaultKey, NostrEvent ev)
+ private bool SignMessage(ReadOnlySpan<char> vaultKey, NostrEvent ev)
{
//Decode the key
int keyBufSize = _keyEncoder.GetKeyBufferSize(vaultKey);
@@ -206,7 +211,6 @@ namespace NVault.Plugins.Vault
{
//Zero the key buffer and key
MemoryUtil.InitializeBlock(buffHandle.Span);
- vaultKey.Erase();
}
}
@@ -268,6 +272,174 @@ namespace NVault.Plugins.Vault
return JavaScriptEncoder.Create(s);
}
+ ///<inheritdoc/>
+ public async Task<string?> DecryptNoteAsync(VaultUserScope scope, NostrKeyMeta keyMeta, string targetPubKeyHex, string nip04Ciphertext, CancellationToken cancellation)
+ {
+ //Recover target public key
+ byte[] targetPubkey = Convert.FromHexString(targetPubKeyHex);
+
+ //Get key data from the vault
+ using PrivateString? secret = await _vault.GetSecretAsync(scope, keyMeta.Id, cancellation);
+
+ if(secret == null)
+ {
+ return null;
+ }
+
+ string? outText = null, ivText = null;
+
+ //Call decipher method
+ bool result = Nip04Cipher(secret.ToReadOnlySpan(), nip04Ciphertext.AsSpan(), targetPubkey, ref outText, ref ivText, false);
+
+ if (result)
+ {
+ return outText;
+ }
+ else
+ {
+ throw new CryptographicException("Failed to decipher the target data");
+ }
+ }
+
+ ///<inheritdoc/>
+ public async Task<EncryptionResult> EncryptNoteAsync(VaultUserScope scope, NostrKeyMeta keyMeta, string targetPubKeyHex, string plainText, CancellationToken cancellation)
+ {
+ //Recover target public key
+ byte[] targetPubkey = Convert.FromHexString(targetPubKeyHex);
+
+ //Get key data from the vault
+ using PrivateString? secret = await _vault.GetSecretAsync(scope, keyMeta.Id, cancellation);
+
+ string? outputText = null,
+ ivText = null;
+
+ //Call decipher method
+ bool result = Nip04Cipher(secret.ToReadOnlySpan(), plainText, targetPubkey, ref outputText, ref ivText, true);
+
+ if (result)
+ {
+ return new()
+ {
+ CipherText = outputText,
+ Iv = ivText
+ };
+ }
+ else
+ {
+ throw new CryptographicException("Failed to encipher the target data");
+ }
+ }
+
+ private bool Nip04Cipher(
+ ReadOnlySpan<char> vaultKey,
+ ReadOnlySpan<char> text,
+ ReadOnlySpan<byte> pubKey,
+ ref string? outputText,
+ ref string? ivText,
+ bool encipher
+ )
+ {
+ //Decode the key
+ int keyBufSize = _keyEncoder.GetKeyBufferSize(vaultKey);
+
+ int maxCtBufferSize = Base64.GetMaxEncodedToUtf8Length(text.Length);
+
+ //Alloc heap buffers for encoding/decoding plaintext
+ using UnsafeMemoryHandle<byte> ctBuffer = MemoryUtil.UnsafeAllocNearestPage(maxCtBufferSize, true);
+ using UnsafeMemoryHandle<byte> outputBuffer = MemoryUtil.UnsafeAllocNearestPage(maxCtBufferSize, true);
+
+ //Small buffers for private key and raw iv
+ Span<byte> privKeyBytes = stackalloc byte[keyBufSize];
+ Span<byte> ivBuffer = stackalloc byte[encipher ? NIP04_RANDOM_IV_SIZE : 64];
+
+ try
+ {
+ //Decode the key
+ ERRNO keySize = _keyEncoder.DecodeKey(vaultKey, privKeyBytes);
+
+ if (encipher)
+ {
+ //Fill IV with randomness
+ _cryptoProvider.GetRandomBytes(ivBuffer);
+
+ //encode to utf8 before ecryption
+ int encodedSize = Encoding.UTF8.GetBytes(text, ctBuffer.Span);
+
+ //Encrypt the message
+ ERRNO outputSize = _cryptoProvider.EncryptMessage(
+ privKeyBytes[..(int)keySize],
+ pubKey,
+ ivBuffer,
+ ctBuffer.AsSpan(0, encodedSize),
+ outputBuffer.Span
+ );
+
+ if (outputSize < 1)
+ {
+ throw new CryptographicException("Failed to encipher message");
+ }
+
+ //Output text is the ciphertext base64 utf8 encoded
+ outputText = Convert.ToBase64String(outputBuffer.AsSpan(0, outputSize));
+ ivText = Convert.ToBase64String(ivBuffer);
+
+ return true;
+ }
+ else
+ {
+ //Text parameter is nostr encoded
+ ReadOnlySpan<char> cipherText = text.SliceBeforeParam("?iv=");
+ ReadOnlySpan<char> ivSegment = text.SliceAfterParam("?iv=");
+
+ if (ivSegment.Length > 128)
+ {
+ throw new ArgumentException("initialization vector is larger than allowed");
+ }
+
+ //Decode initialziation vector
+ ERRNO ivSize= VnEncoding.TryFromBase64Chars(ivSegment, ivBuffer);
+ if (ivSize < 1)
+ {
+ return false;
+ }
+
+ //Decode ciphertext
+ ERRNO ctSize = VnEncoding.TryFromBase64Chars(cipherText, ctBuffer.Span);
+ if (ctSize < 1)
+ {
+ return false;
+ }
+
+ //Decrypt the message
+ ERRNO outputSize = _cryptoProvider.DecryptMessage(
+ privKeyBytes,
+ pubKey,
+ ivBuffer.Slice(0, ivSize),
+ ctBuffer.AsSpan(0, ctSize),
+ outputBuffer.Span
+ );
+
+ if (outputSize < 1)
+ {
+ return false;
+ }
+
+ //Store the output text (deciphered text)
+ outputText = Encoding.UTF8.GetString(outputBuffer.AsSpan(0, outputSize));
+
+ return true;
+ }
+ }
+ finally
+ {
+ //Zero the key buffer and key
+ MemoryUtil.InitializeBlock(ctBuffer.Span);
+ MemoryUtil.InitializeBlock(outputBuffer.Span);
+ MemoryUtil.InitializeBlock(privKeyBytes);
+ MemoryUtil.InitializeBlock(ivBuffer);
+ }
+ }
+
readonly record struct EvBuffer(IMemoryHandle<byte> Handle, int KeySize, int SigSize, int HashSize)
{
public readonly Span<byte> KeyBuffer => Handle.Span[..KeySize];