diff options
author | vnugent <public@vaughnnugent.com> | 2023-11-19 14:50:46 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-11-19 14:50:46 -0500 |
commit | bc7b86a242673d7831f6105d000995d9f4d63e09 (patch) | |
tree | 8da5c92047e92174b80ff6f460f8c3148e1e00ca /back-end | |
parent | 0b609c17199e937518c42365b360288acfa872be (diff) |
hasty not working update to get my workspace clean
Diffstat (limited to 'back-end')
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]; |