aboutsummaryrefslogtreecommitdiff
path: root/back-end/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'back-end/plugins')
-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
8 files changed, 500 insertions, 12 deletions
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];