From bc7b86a242673d7831f6105d000995d9f4d63e09 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 19 Nov 2023 14:50:46 -0500 Subject: hasty not working update to get my workspace clean --- .../src/ContextExtensions.cs | 110 ++++++++++++- .../NVault.Crypto.Secp256k1/src/LibSecp256k1.cs | 45 ++++-- .../src/NVault.Crypto.Secp256k1.csproj | 4 +- .../src/NVault.VaultExtensions.csproj | 2 +- .../src/VaultClientExtensions.cs | 2 +- back-end/plugins/nvault/src/EncryptionResult.cs | 28 ++++ back-end/plugins/nvault/src/Endpoints/Endpoint.cs | 172 +++++++++++++++++++- .../plugins/nvault/src/INostrCryptoProvider.cs | 10 ++ back-end/plugins/nvault/src/INostrOperations.cs | 5 + .../plugins/nvault/src/ManagedCryptoprovider.cs | 20 ++- back-end/plugins/nvault/src/NVault.csproj | 7 +- .../plugins/nvault/src/NativeSecp256k1Library.cs | 92 ++++++++++- back-end/plugins/nvault/src/NostrOpProvider.cs | 178 ++++++++++++++++++++- 13 files changed, 646 insertions(+), 29 deletions(-) create mode 100644 back-end/plugins/nvault/src/EncryptionResult.cs (limited to 'back-end') 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 + { + + /// + /// The opaque pointer passed to the hash function + /// + 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; + } + + /// + /// Gets the output buffer as a span + /// + /// The output buffer span + public readonly Span GetOutput() => new(_output, _outputLength); + + /// + /// Gets the x coordinate argument as a span + /// + /// The xcoordinate buffer span + public readonly ReadOnlySpan GetXCoordArg() => new(_xCoord, _xCoordLength); + } + public static unsafe class ContextExtensions { /// @@ -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; } + } + + /// + /// Verifies that a given secret key is valid using the current context + /// + /// + /// The secret key to verify + /// A boolean value that indicates if the secret key is valid or not + /// + public static bool ComputeSharedKey(this in Secp256k1Context context, Span data, ReadOnlySpan xOnlyPubKey, ReadOnlySpan 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(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 + /// + /// 1:1 with the secp256k1_pubkey structure + /// + [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 + ); + + + /// /// Loads the Secp256k1 library from the specified path and creates a wrapper class (loads methods from the library) /// @@ -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; /// @@ -148,6 +169,8 @@ namespace NVault.Crypto.Secp256k1 _serializeXonly = handle.DangerousGetMethod(); _signHash = handle.DangerousGetMethod(); _secKeyVerify = handle.DangerousGetMethod(); + _pubKeySerialize = handle.DangerousGetMethod(); + _ecdh = handle.DangerousGetMethod(); //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 @@ - - + + 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 @@ - + 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); } /// 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 . + +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 RelayValidator { get; } = NostrRelay.GetValidator(); private static IValidator KeyMetaValidator { get; } = NostrKeyMeta.GetValidator(); private static IValidator CreateKeyRequestValidator { get; } = CreateKeyRequest.GetValidator(); + private static IValidator DecrptMessageValidator { get; } = Nip04DecryptRequest.GetValidator(); + private static IValidator 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 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(); @@ -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(); + + 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(); + + 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 GetValidator() + { + InlineValidator 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 GetValidator() + { + InlineValidator 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 /// The recovered public key /// True if the operation succeeded, false otherwise bool RecoverPublicKey(ReadOnlySpan privateKey, Span pubKey); + + ERRNO DecryptMessage(ReadOnlySpan secretKey, ReadOnlySpan targetKey, ReadOnlySpan aseIv, ReadOnlySpan cyphterText, Span outputBuffer); + + ERRNO EncryptMessage(ReadOnlySpan secretKey, ReadOnlySpan targetKey, ReadOnlySpan aesIv, ReadOnlySpan plainText, Span cipherText); + + /// + /// Fill a buffer with secure randomness/entropy + /// + /// A span of memory to fill with random data + void GetRandomBytes(Span 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 SignEventAsync(VaultUserScope scope, NostrKeyMeta keyMeta, NostrEvent evnt, CancellationToken cancellation); @@ -31,5 +32,9 @@ namespace NVault.Plugins.Vault Task CreateFromExistingAsync(VaultUserScope scope, NostrKeyMeta newKey, string hexKey, CancellationToken cancellation); Task DeleteCredentialAsync(VaultUserScope scope, NostrKeyMeta key, CancellationToken cancellation); + + Task DecryptNoteAsync(VaultUserScope scope, NostrKeyMeta key, string targetPubKeyHex, string nip04Ciphertext, CancellationToken cancellation); + + Task 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(path).Resource; + random = plugin.CreateServiceExternal(path); } else { @@ -77,5 +77,23 @@ namespace NVault.Plugins.Vault /// public bool RecoverPublicKey(ReadOnlySpan privateKey, Span pubKey) => _provider.RecoverPublicKey(privateKey, pubKey); + + /// + public ERRNO DecryptMessage(ReadOnlySpan secretKey, ReadOnlySpan targetKey, ReadOnlySpan aseIv, ReadOnlySpan cyphterText, Span outputBuffer) + { + return _provider.DecryptMessage(secretKey, targetKey, aseIv, cyphterText, outputBuffer); + } + + /// + public ERRNO EncryptMessage(ReadOnlySpan secretKey, ReadOnlySpan targetKey, ReadOnlySpan aesIv, ReadOnlySpan plainText, Span cipherText) + { + return _provider.EncryptMessage(secretKey, targetKey, aesIv, plainText, cipherText); + } + + /// + public void GetRandomBytes(Span 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 @@ - - + + @@ -31,9 +31,10 @@ - + Always + 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 . 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 /// The loaded 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); } + /// + public ERRNO DecryptMessage(ReadOnlySpan secretKey, ReadOnlySpan targetKey, ReadOnlySpan aesIv, ReadOnlySpan ciphterText, Span 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()); + } + } + + /// + public ERRNO EncryptMessage(ReadOnlySpan secretKey, ReadOnlySpan targetKey, ReadOnlySpan aesIv, ReadOnlySpan plainText, Span 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 sharedKey = state.GetOutput(); + ReadOnlySpan 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 /// public KeyBufferSizes GetKeyBufferSize() => new(LibSecp256k1.SecretKeySize, LibSecp256k1.XOnlyPublicKeySize); @@ -86,6 +171,9 @@ namespace NVault.Plugins.Vault return context.SchnorSignDigest(key, digest, signatureBuffer); } + /// + public void GetRandomBytes(Span bytes) => _lib.GetRandomBytes(bytes); + /// public bool TryGenerateKeyPair(Span publicKey, Span privateKey) { @@ -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 . 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 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 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); } + /// + public async Task 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"); + } + } + + /// + public async Task 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 vaultKey, + ReadOnlySpan text, + ReadOnlySpan 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 ctBuffer = MemoryUtil.UnsafeAllocNearestPage(maxCtBufferSize, true); + using UnsafeMemoryHandle outputBuffer = MemoryUtil.UnsafeAllocNearestPage(maxCtBufferSize, true); + + //Small buffers for private key and raw iv + Span privKeyBytes = stackalloc byte[keyBufSize]; + Span 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 cipherText = text.SliceBeforeParam("?iv="); + ReadOnlySpan 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 Handle, int KeySize, int SigSize, int HashSize) { public readonly Span KeyBuffer => Handle.Span[..KeySize]; -- cgit