diff options
21 files changed, 922 insertions, 361 deletions
diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Base64SignatureEncoder.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Base64SignatureEncoder.cs new file mode 100644 index 0000000..c5078a4 --- /dev/null +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Base64SignatureEncoder.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; + +namespace VNLib.Utils.Cryptography.Noscrypt +{ + public sealed class Base64SignatureEncoder : INostrSignatureEncoder + { + /// <summary> + /// Shared formatter instance for base64 signatures + /// </summary> + public static Base64SignatureEncoder Instance { get; } = new Base64SignatureEncoder(); + + ///<inheritdoc/> + public string GetString(ReadOnlySpan<byte> signature) => Convert.ToBase64String(signature); + } + +} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/HexSignatureEncoder.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/HexSignatureEncoder.cs new file mode 100644 index 0000000..6a60c73 --- /dev/null +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/HexSignatureEncoder.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; + +namespace VNLib.Utils.Cryptography.Noscrypt +{ + public sealed class HexSignatureEncoder : INostrSignatureEncoder + { + /// <summary> + /// Shared formatter instance for hex signatures + /// </summary> + public static HexSignatureEncoder Instance { get; } = new HexSignatureEncoder(); + + ///<inheritdoc/> + public string GetString(ReadOnlySpan<byte> signature) => Convert.ToHexString(signature); + } + +} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/IEncryptionVersion.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/INostrEncryptionVersion.cs index e9aab2b..3a26466 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/IEncryptionVersion.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/INostrEncryptionVersion.cs @@ -18,7 +18,7 @@ namespace VNLib.Utils.Cryptography.Noscrypt /// <summary> /// Represents a message encryption version used by the Nostr protocol /// </summary> - public interface IEncryptionVersion + public interface INostrEncryptionVersion { /// <summary> /// The noscrypt compatible encryption version @@ -26,11 +26,18 @@ namespace VNLib.Utils.Cryptography.Noscrypt internal uint Version { get; } /// <summary> - /// Calculates the required buffer size for the specified data size + /// Calculates the required payload buffer size for the specified data size /// </summary> /// <param name="dataSize">The size of the input data</param> /// <returns>The estimated size of the buffer required to complete the opeation</returns> - internal int CalcBufferSize(int dataSize); + internal int GetPayloadBufferSize(int dataSize); + + /// <summary> + /// Calculates the required message buffer size for the specified data size + /// </summary> + /// <param name="dataSize">Plain text data size</param> + /// <returns>The estimated size of the buffer required to complete the opeation</returns> + internal int GetMessageBufferSize(int dataSize); } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/INostrSignatureEncoder.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/INostrSignatureEncoder.cs new file mode 100644 index 0000000..b8c69f5 --- /dev/null +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/INostrSignatureEncoder.cs @@ -0,0 +1,33 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; + +namespace VNLib.Utils.Cryptography.Noscrypt +{ + /// <summary> + /// Encodes a message signature into it's string representation + /// </summary> + public interface INostrSignatureEncoder + { + /// <summary> + /// Creates a string of the encoded signature data + /// </summary> + /// <param name="signature">The signature data to encode into the string</param> + /// <returns>The encoded signature string</returns> + string GetString(ReadOnlySpan<byte> signature); + } + +} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCContext.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCContext.cs index dbd9372..8f8c6b4 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCContext.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCContext.cs @@ -21,17 +21,16 @@ using Microsoft.Win32.SafeHandles; using VNLib.Utils.Extensions; using VNLib.Utils.Memory; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using VNLib.Utils.Cryptography.Noscrypt.@internal; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; using NCResult = System.Int64; namespace VNLib.Utils.Cryptography.Noscrypt { /// <summary> - /// Represents a context for the native library + /// The noscrypt library context /// </summary> - /// <param name="Heap">The heap the handle was allocated from</param> - /// <param name="Library">A reference to the native library</param> public sealed class NCContext : SafeHandleZeroOrMinusOneIsInvalid { private readonly IUnmangedHeap Heap; @@ -39,9 +38,9 @@ namespace VNLib.Utils.Cryptography.Noscrypt /// <summary> /// The library this context was created from /// </summary> - public LibNoscrypt Library { get; } + public NoscryptLibrary Library { get; } - internal NCContext(IntPtr handle, IUnmangedHeap heap, LibNoscrypt library) :base(true) + internal NCContext(IntPtr handle, IUnmangedHeap heap, NoscryptLibrary library) :base(true) { ArgumentNullException.ThrowIfNull(heap); ArgumentNullException.ThrowIfNull(library); @@ -61,7 +60,7 @@ namespace VNLib.Utils.Cryptography.Noscrypt public unsafe void Reinitalize(ref byte entropy, int size) { //Entropy must be exactly 32 bytes - ArgumentOutOfRangeException.ThrowIfNotEqual(size, CTX_ENTROPY_SIZE); + ArgumentOutOfRangeException.ThrowIfNotEqual(size, NC_CTX_ENTROPY_SIZE); this.ThrowIfClosed(); fixed (byte* p = &entropy) diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip04EncryptionVersion.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip04EncryptionVersion.cs index 69234fe..beb21c2 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip04EncryptionVersion.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip04EncryptionVersion.cs @@ -13,14 +13,14 @@ // 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 static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { /// <summary> /// The NIP04 encryption version used by the Nostr protocol /// </summary> - public sealed class NCNip04EncryptionVersion : IEncryptionVersion + public sealed class NCNip04EncryptionVersion : INostrEncryptionVersion { /// <summary> /// A static nip04 encryption version instance @@ -28,10 +28,13 @@ namespace VNLib.Utils.Cryptography.Noscrypt public static readonly NCNip04EncryptionVersion Instance = new(); ///<inheritdoc/> - uint IEncryptionVersion.Version => NC_ENC_VERSION_NIP04; - + uint INostrEncryptionVersion.Version => NC_ENC_VERSION_NIP04; + + ///<inheritdoc/> + int INostrEncryptionVersion.GetMessageBufferSize(int dataSize) => Nip04Util.CalcBufferSize(dataSize); + ///<inheritdoc/> - int IEncryptionVersion.CalcBufferSize(int dataSize) => Nip04Util.CalcBufferSize(dataSize); + int INostrEncryptionVersion.GetPayloadBufferSize(int dataSize) => Nip04Util.CalcBufferSize(dataSize); } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip44EncryptionVersion.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip44EncryptionVersion.cs index e572dd7..0d5907a 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip44EncryptionVersion.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCNip44EncryptionVersion.cs @@ -13,14 +13,14 @@ // 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 static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { /// <summary> /// The NIP44 encryption version used by the Nostr protocol /// </summary> - public sealed class NCNip44EncryptionVersion : IEncryptionVersion + public sealed class NCNip44EncryptionVersion : INostrEncryptionVersion { /// <summary> /// A static nip44 encryption version instance @@ -28,10 +28,12 @@ namespace VNLib.Utils.Cryptography.Noscrypt public static readonly NCNip44EncryptionVersion Instance = new(); ///<inheritdoc/> - uint IEncryptionVersion.Version => NC_ENC_VERSION_NIP44; + uint INostrEncryptionVersion.Version => NC_ENC_VERSION_NIP44; + + int INostrEncryptionVersion.GetMessageBufferSize(int dataSize) => Nip44Util.CalcFinalBufferSize(dataSize); ///<inheritdoc/> - int IEncryptionVersion.CalcBufferSize(int dataSize) => Nip44Util.CalcBufferSize(dataSize); + int INostrEncryptionVersion.GetPayloadBufferSize(int dataSize) => Nip44Util.CalcBufferSize(dataSize); } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCPublicKey.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCPublicKey.cs index 0699d7d..57d7c3f 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCPublicKey.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCPublicKey.cs @@ -14,9 +14,10 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { @@ -26,6 +27,11 @@ namespace VNLib.Utils.Cryptography.Noscrypt [StructLayout(LayoutKind.Sequential, Size = NC_SEC_PUBKEY_SIZE)] public unsafe struct NCPublicKey { + /// <summary> + /// Gets a null <see cref="NCPublicKey"/> reference. + /// </summary> + public static ref NCPublicKey NullRef => ref Unsafe.NullRef<NCPublicKey>(); + private fixed byte key[NC_SEC_PUBKEY_SIZE]; } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCSecretKey.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCSecretKey.cs index b586d26..18f025b 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCSecretKey.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCSecretKey.cs @@ -13,9 +13,10 @@ // 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.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { @@ -26,6 +27,11 @@ namespace VNLib.Utils.Cryptography.Noscrypt [StructLayout(LayoutKind.Sequential, Size = NC_SEC_KEY_SIZE)] public unsafe struct NCSecretKey { + /// <summary> + /// Gets a null reference to a secret key + /// </summary> + public static ref NCSecretKey NullRef => ref Unsafe.NullRef<NCSecretKey>(); + private fixed byte key[NC_SEC_KEY_SIZE]; } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCUtil.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCUtil.cs index e212125..49c66c1 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCUtil.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NCUtil.cs @@ -18,7 +18,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; using NCResult = System.Int64; diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NcFallbackRandom.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NcFallbackRandom.cs new file mode 100644 index 0000000..0949ad8 --- /dev/null +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NcFallbackRandom.cs @@ -0,0 +1,36 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; + +using VNLib.Hashing; + +namespace VNLib.Utils.Cryptography.Noscrypt +{ + /// <summary> + /// A fallback crypographic random source used for default + /// rng if you wish + /// </summary> + public sealed class NcFallbackRandom : IRandomSource + { + /// <summary> + /// Gets the shared instance of the fallback random source + /// </summary> + public static NcFallbackRandom Shared { get; } = new NcFallbackRandom(); + + /// <inheritdoc/> + public void GetRandomBytes(Span<byte> buffer) => RandomHash.GetRandomBytes(buffer); + } +} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip04Util.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip04Util.cs index 82704db..c1906f0 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip04Util.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip04Util.cs @@ -14,6 +14,7 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. using System; +using System.Buffers.Text; using VNLib.Utils.Extensions; @@ -36,6 +37,14 @@ namespace VNLib.Utils.Cryptography.Noscrypt { throw new NotImplementedException(); } + + static readonly int MaxEncodedIvLength = Base64.GetMaxEncodedToUtf8Length(16); + + public static int CalcMessageBufferSize(int dataSize) + { + int bufSize = CalcBufferSize(dataSize); + return bufSize + "?iv=".Length + MaxEncodedIvLength; + } } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44Message.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44MessageSegments.cs index b78530b..ddc2d68 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44Message.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44MessageSegments.cs @@ -17,7 +17,7 @@ using System; namespace VNLib.Utils.Cryptography.Noscrypt { - public readonly ref struct Nip44Message(ReadOnlySpan<byte> payload) + public readonly ref struct Nip44MessageSegments(ReadOnlySpan<byte> payload) { readonly ReadOnlySpan<byte> _payload = payload; diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44Util.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44Util.cs index 23c9127..2aebee1 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44Util.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/Nip44Util.cs @@ -19,7 +19,7 @@ using System.Runtime.InteropServices; using VNLib.Utils.Memory; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { @@ -92,16 +92,51 @@ namespace VNLib.Utils.Cryptography.Noscrypt //Copy the plaintext data to the output buffer after the data size MemoryUtil.Memmove( - in MemoryMarshal.GetReference(plaintextData), - 0, - ref MemoryMarshal.GetReference(output), - sizeof(ushort), - (uint)plaintextData.Length + src: in MemoryMarshal.GetReference(plaintextData), + srcOffset: 0, + dst: ref MemoryMarshal.GetReference(output), + dstOffset: sizeof(ushort), + elementCount: (uint)plaintextData.Length ); //We assume the remaining buffer is zeroed out } + public static void WriteNip44Message( + ReadOnlySpan<byte> payloadBuffer, + byte version, + ReadOnlySpan<byte> mac, + Span<byte> outBuffer + ) + { + int requiredBufferSize = CalcFinalBufferSize(payloadBuffer.Length); + + //Make sure the output buffer is large enough so we dont overrun it + ArgumentOutOfRangeException.ThrowIfLessThan(outBuffer.Length, requiredBufferSize, nameof(outBuffer)); + ArgumentOutOfRangeException.ThrowIfLessThan(mac.Length, NC_ENCRYPTION_MAC_SIZE, nameof(mac)); + + //Write the version number to the first byte + outBuffer[0] = version; + + //Copy the payload buffer to the output buffer after the version number + MemoryUtil.Memmove( + src: in MemoryMarshal.GetReference(payloadBuffer), + srcOffset: 0, + dst: ref MemoryMarshal.GetReference(outBuffer), + dstOffset: 1, + elementCount: (uint)payloadBuffer.Length + ); + + //Copy the mac to the end of the output buffer + MemoryUtil.Memmove( + src: in MemoryMarshal.GetReference(mac), + srcOffset: 0, + dst: ref MemoryMarshal.GetReference(outBuffer), + dstOffset: (uint)(requiredBufferSize - NC_ENCRYPTION_MAC_SIZE), + elementCount: NC_ENCRYPTION_MAC_SIZE + ); + } + public static ReadOnlySpan<byte> GetNonceFromPayload(ReadOnlySpan<byte> message) { //The nonce is 32 bytes following the 1st byte version number of the message @@ -139,6 +174,17 @@ namespace VNLib.Utils.Cryptography.Noscrypt return plaintextPayload.Slice(sizeof(ushort), ptLength); } + public static bool IsValidPlaintextMessage(ReadOnlySpan<byte> plaintextPayload) + { + ushort ptLength = BinaryPrimitives.ReadUInt16BigEndian(plaintextPayload); + return ptLength == plaintextPayload.Length - sizeof(ushort); + } + + public static Range GetPlaintextRange(ReadOnlySpan<byte> plaintextPayload) + { + ushort ptLength = BinaryPrimitives.ReadUInt16BigEndian(plaintextPayload); + return new Range(sizeof(ushort), ptLength); + } } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptExtensions.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptExtensions.cs index aaabc08..89bd057 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptExtensions.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptExtensions.cs @@ -16,7 +16,7 @@ using System; using System.Runtime.InteropServices; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/LibNoscrypt.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptLibrary.cs index 32a07f4..108a713 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/LibNoscrypt.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptLibrary.cs @@ -16,11 +16,9 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; - -using VNLib.Utils; -using VNLib.Utils.Extensions; using VNLib.Utils.Memory; using VNLib.Utils.Native; +using VNLib.Utils.Extensions; using VNLib.Utils.Cryptography.Noscrypt.@internal; @@ -34,37 +32,41 @@ namespace VNLib.Utils.Cryptography.Noscrypt /// </summary> /// <param name="Library">An existing noscrypt library handle</param> /// <param name="OwnsHandle">A value that indicates if the instance owns the library handle</param> - public unsafe sealed class LibNoscrypt(SafeLibraryHandle Library, bool OwnsHandle) : VnDisposeable + public unsafe sealed class NoscryptLibrary(SafeLibraryHandle Library, bool OwnsHandle) : VnDisposeable { - //Values that match the noscrypt.h header - public const int NC_SEC_KEY_SIZE = 32; - public const int NC_SEC_PUBKEY_SIZE = 32; - public const int NC_ENCRYPTION_NONCE_SIZE = 32; - public const int NC_PUBKEY_SIZE = 32; - public const int NC_SIGNATURE_SIZE = 64; - public const int NC_CONV_KEY_SIZE = 32; - public const int NC_MESSAGE_KEY_SIZE = 32; - public const int NC_HMAC_KEY_SIZE = 32; - public const int NC_ENCRYPTION_MAC_SIZE = 32; - public const int NC_CONVERSATION_KEY_SIZE = 32; - public const int CTX_ENTROPY_SIZE = 32; - - public const uint NC_ENC_VERSION_NIP04 = 0x00000004u; - public const uint NC_ENC_VERSION_NIP44 = 0x00000002c; - - public const uint NC_ENC_SET_VERSION = 0x01u; - public const uint NC_ENC_SET_NIP44_NONCE = 0x02u; - public const uint NC_ENC_SET_NIP44_MAC_KEY = 0x03u; - public const uint NC_ENC_SET_NIP04_KEY = 0x04u; - public const uint NC_ENC_SET_NIP04_IV = 0x05u; - - public const NCResult NC_SUCCESS = 0; - public const byte E_NULL_PTR = 0x01; - public const byte E_INVALID_ARG = 0x02; - public const byte E_INVALID_CTX = 0x03; - public const byte E_ARGUMENT_OUT_OF_RANGE = 0x04; - public const byte E_OPERATION_FAILED = 0x05; - public const byte E_VERSION_NOT_SUPPORTED = 0x06; + public const string NoscryptDefaultLibraryName = "noscrypt"; + + //Constant values match the noscrypt.h header + public const int NC_SEC_KEY_SIZE = 0x20; + public const int NC_SEC_PUBKEY_SIZE = 0x20; + public const int NC_ENCRYPTION_NONCE_SIZE = 0x20; + public const int NC_PUBKEY_SIZE = 0x20; + public const int NC_SIGNATURE_SIZE = 0x40; + public const int NC_CONV_KEY_SIZE = 0x20; + public const int NC_MESSAGE_KEY_SIZE = 0x20; + public const int NC_HMAC_KEY_SIZE = 0x20; + public const int NC_ENCRYPTION_MAC_SIZE = 0x20; + public const int NC_CONVERSATION_KEY_SIZE = 0x20; + public const int NC_CTX_ENTROPY_SIZE = 0x20; + public const int NC_SIG_ENTROPY_SIZE = 0x20; + + public const uint NC_ENC_VERSION_NIP04 = 0x00000004u; + public const uint NC_ENC_VERSION_NIP44 = 0x00000002c; + + public const uint NC_ENC_SET_VERSION = 0x01u; + public const uint NC_ENC_SET_NIP44_NONCE = 0x02u; + public const uint NC_ENC_SET_NIP44_MAC_KEY = 0x03u; + public const uint NC_ENC_SET_NIP04_KEY = 0x04u; + public const uint NC_ENC_SET_NIP04_IV = 0x05u; + + //Noscrypt error codes + public const NCResult NC_SUCCESS = 0x00; + public const byte E_NULL_PTR = 0x01; + public const byte E_INVALID_ARG = 0x02; + public const byte E_INVALID_CTX = 0x03; + public const byte E_ARGUMENT_OUT_OF_RANGE = 0x04; + public const byte E_OPERATION_FAILED = 0x05; + public const byte E_VERSION_NOT_SUPPORTED = 0x06; private readonly FunctionTable _functions = FunctionTable.BuildFunctionTable(Library); @@ -103,7 +105,7 @@ namespace VNLib.Utils.Cryptography.Noscrypt ArgumentNullException.ThrowIfNull(heap); //Entropy must be exactly 32 bytes - ArgumentOutOfRangeException.ThrowIfNotEqual(size, CTX_ENTROPY_SIZE); + ArgumentOutOfRangeException.ThrowIfNotEqual(size, NC_CTX_ENTROPY_SIZE); //Get struct size nuint ctxSize = Functions.NCGetContextStructSize.Invoke(); @@ -155,7 +157,6 @@ namespace VNLib.Utils.Cryptography.Noscrypt /// This may be done once at app startup and is thread-safe for the rest of the /// application lifetime. /// </summary> - /// <param name="library"></param> /// <param name="heap">The heap to allocate the context from</param> /// <param name="entropy32">The random entropy data to initialize the context with</param> /// <returns>The library wrapper handle</returns> @@ -170,6 +171,29 @@ namespace VNLib.Utils.Cryptography.Noscrypt ); } + /// <summary> + /// Initializes a new NostrCrypto context wraper directly that owns the internal context. + /// This may be done once at app startup and is thread-safe for the rest of the + /// application lifetime. + /// </summary> + /// <param name="heap">The heap to allocate the context from</param> + /// <param name="random">Random source used to generate context entropy</param> + /// <returns>The library wrapper handle</returns> + public NostrCrypto InitializeCrypto(IUnmangedHeap heap, IRandomSource random) + { + ArgumentNullException.ThrowIfNull(random); + + //Get random bytes for context entropy + Span<byte> entropy = stackalloc byte[NC_CTX_ENTROPY_SIZE]; + random.GetRandomBytes(entropy); + + NostrCrypto nc = InitializeCrypto(heap, entropy); + + MemoryUtil.InitializeBlock(entropy); + + return nc; + } + ///<inheritdoc/> protected override void Free() { @@ -187,7 +211,8 @@ namespace VNLib.Utils.Cryptography.Noscrypt /// <param name="path">The native library path or name to load</param> /// <param name="search">The search path options</param> /// <returns>The loaded library instance</returns> - public static LibNoscrypt Load(string path, DllImportSearchPath search) + /// <exception cref="DllNotFoundException"></exception> + public static NoscryptLibrary Load(string path, DllImportSearchPath search) { //Load the native library SafeLibraryHandle handle = SafeLibraryHandle.LoadLibrary(path, search); @@ -195,7 +220,7 @@ namespace VNLib.Utils.Cryptography.Noscrypt Trace.WriteLine($"Loaded noscrypt library 0x{handle.DangerousGetHandle():x} from {path}"); //Create the wrapper - return new LibNoscrypt(handle, true); + return new NoscryptLibrary(handle, true); } /// <summary> @@ -204,7 +229,14 @@ namespace VNLib.Utils.Cryptography.Noscrypt /// </summary> /// <param name="path">The native library path or name to load</param> /// <returns>The loaded library instance</returns> - public static LibNoscrypt Load(string path) => Load(path, DllImportSearchPath.SafeDirectories); - + /// <exception cref="DllNotFoundException"></exception> + public static NoscryptLibrary Load(string path) => Load(path, DllImportSearchPath.SafeDirectories); + + /// <summary> + /// Attempts to load the default noscrypt library from the system search path + /// </summary> + /// <returns>The loaded library instance</returns> + /// <exception cref="DllNotFoundException"></exception> + public static NoscryptLibrary LoadDefault() => Load(NoscryptDefaultLibraryName, DllImportSearchPath.SafeDirectories); } } diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptSigner.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptSigner.cs new file mode 100644 index 0000000..586fa46 --- /dev/null +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NoscryptSigner.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; + +using VNLib.Utils.Extensions; +using VNLib.Utils.Memory; + +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; + +namespace VNLib.Utils.Cryptography.Noscrypt +{ + /// <summary> + /// A simple wrapper class to sign nostr message data using + /// the noscrypt library + /// </summary> + /// <param name="noscrypt">The noscrypt library instance</param> + /// <param name="random">A random entropy pool used to source random data for signature entropy</param> + public class NoscryptSigner(INostrCrypto noscrypt, IRandomSource random) + { + /// <summary> + /// Gets the size of the buffer required to hold the signature + /// </summary> + public static int SignatureBufferSize => NC_SIGNATURE_SIZE; + + /// <summary> + /// Signs a message using the specified private key and message data + /// </summary> + /// <param name="hexPrivateKey">The hexadecimal private key used to sign the message</param> + /// <param name="message">The message data to sign</param> + /// <param name="format">A encoder used to convert the signature data to an encoded string</param> + /// <returns>The string encoded nostr signature</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public string SignData(string hexPrivateKey, ReadOnlySpan<byte> message, INostrSignatureEncoder? format = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(hexPrivateKey); + ArgumentOutOfRangeException.ThrowIfNotEqual(hexPrivateKey.Length / 2, NC_SEC_KEY_SIZE, nameof(hexPrivateKey)); + + //Have to allocate array unfortunately + byte[] privKey = Convert.FromHexString(hexPrivateKey); + try + { + return SignData(privKey.AsSpan(), message, format); + } + finally + { + //Always zero key beofre leaving + MemoryUtil.InitializeBlock(privKey); + } + } + + /// <summary> + /// Signs a message using the specified secret key and message data + /// </summary> + /// <param name="secretKey">The secret key data buffer</param> + /// <param name="message">The message data to sign</param> + /// <param name="format">A encoder used to convert the signature data to an encoded string</param> + /// <returns>The string encoded nostr signature</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public string SignData( + ReadOnlySpan<byte> secretKey, + ReadOnlySpan<byte> message, + INostrSignatureEncoder? format = null + ) + { + return SignData(in NCUtil.AsSecretKey(secretKey), message, format); + } + + /// <summary> + /// Signs a message using the specified secret key and message data + /// </summary> + /// <param name="secretkey">A reference to the secret key structurer</param> + /// <param name="message">The message data to sign</param> + /// <param name="format">A encoder used to convert the signature data to an encoded string</param> + /// <returns>The string encoded nostr signature</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public string SignData( + ref readonly NCSecretKey secretkey, + ReadOnlySpan<byte> message, + INostrSignatureEncoder? format = null + ) + { + //Default to hex encoding because that is the default NIP-01 format + format ??= HexSignatureEncoder.Instance; + + Span<byte> sigBuffer = stackalloc byte[SignatureBufferSize]; + + SignData(message, sigBuffer); + + return format.GetString(sigBuffer); + } + + + /// <summary> + /// Signs a message using the specified secret key and message data + /// </summary> + /// <param name="secretkey">A reference to the secret key structurer</param> + /// <param name="data">The message data to sign</param> + /// <param name="signature">A buffer to write signature data to</param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public void SignData( + ref readonly NCSecretKey secretkey, + ReadOnlySpan<byte> data, + Span<byte> signature + ) + { + ArgumentOutOfRangeException.ThrowIfLessThan(signature.Length, NC_SIGNATURE_SIZE, nameof(signature)); + + //Signature generation required random entropy to be secure + Span<byte> entropy = stackalloc byte[NC_SIG_ENTROPY_SIZE]; + random.GetRandomBytes(entropy); + + noscrypt.SignData(in secretkey, entropy, data, signature); + } + } + +} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrCrypto.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrCrypto.cs index ec2cf66..36e2381 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrCrypto.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrCrypto.cs @@ -18,7 +18,7 @@ using System.Runtime.CompilerServices; using System.Diagnostics.CodeAnalysis; using VNLib.Utils.Cryptography.Noscrypt.@internal; -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; using NCResult = System.Int64; @@ -86,7 +86,11 @@ namespace VNLib.Utils.Cryptography.Noscrypt fixed (NCSecretKey* pSecKey = &secretKey) fixed (NCPublicKey* pPubKey = &publicKey) - fixed (byte* pCipherText = &cipherText, pTextPtr = &plainText, pHmacKeyOut = &hmackKeyOut32, pNonce = &nonce32) + fixed (byte* pCipherText = &cipherText, + pTextPtr = &plainText, + pHmacKeyOut = &hmackKeyOut32, + pNonce = &nonce32 + ) { NCEncryptionArgs data = new(); @@ -227,7 +231,13 @@ namespace VNLib.Utils.Cryptography.Noscrypt } } - public void ComputeMac(ref readonly byte hmacKey32, ref readonly byte payload, uint payloadSize, ref byte hmacOut32) + ///<inheritdoc/> + public void ComputeMac( + ref readonly byte hmacKey32, + ref readonly byte payload, + uint payloadSize, + ref byte hmacOut32 + ) { Check(); diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrEncryptedMessage.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrEncryptedMessage.cs deleted file mode 100644 index c70839c..0000000 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrEncryptedMessage.cs +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (C) 2024 Vaughn Nugent -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using VNLib.Utils.Extensions; -using VNLib.Utils.Memory; - -using static VNLib.Utils.Cryptography.Noscrypt.LibNoscrypt; - -namespace VNLib.Utils.Cryptography.Noscrypt -{ - - public sealed class NostrEncryptedMessage(IEncryptionVersion version, INostrCrypto lib) : VnDisposeable - { - private readonly INostrCrypto library = lib; - - private NCSecretKey _fromKey; - private NCPublicKey _toKey; - private Buffer32 _nonce32; - - /// <summary> - /// The message encryption version used by this instance - /// </summary> - public uint Version { get; } = version.Version; - - /// <summary> - /// The message nonce created during encryption event - /// </summary> - public unsafe Span<byte> Nonce - { - get - { - Debug.Assert(NC_ENCRYPTION_NONCE_SIZE == sizeof(Buffer32)); - return MemoryMarshal.CreateSpan(ref GetNonceRef(), sizeof(Buffer32)); - } - } - - /// <summary> - /// Gets the size of the buffer required to encrypt the specified data size - /// </summary> - /// <param name="dataSize">The size of the message raw plaintext message to send</param> - /// <returns>The minimum number of bytes required for message encryption output</returns> - /// <exception cref="NotSupportedException"></exception> - public int GetOutputBufferSize(int dataSize) - => version.CalcBufferSize(dataSize); - - /// <summary> - /// Sets the encryption secret key for the message - /// </summary> - /// <param name="secKey">The secret key buffer</param> - /// <returns>The current instance for chaining</returns> - /// <exception cref="ArgumentException"></exception> - public NostrEncryptedMessage SetSecretKey(ReadOnlySpan<byte> secKey) - => SetSecretKey(in NCUtil.AsSecretKey(secKey)); - - /// <summary> - /// Sets the encryption secret key for the message - /// </summary> - /// <param name="secKey">The secret key structure to copy</param> - /// <returns>The current instance for chaining</returns> - /// <exception cref="ArgumentException"></exception> - public NostrEncryptedMessage SetSecretKey(ref readonly NCSecretKey secKey) - { - MemoryUtil.CloneStruct(in secKey, ref _fromKey); - return this; - } - - /// <summary> - /// Assigns the public key used to encrypt the message as the - /// receiver of the message - /// </summary> - /// <param name="pubKey">The user's public key receiving the message</param> - /// <returns>The current instance for chaining</returns> - /// <exception cref="ArgumentException"></exception> - public NostrEncryptedMessage SetPublicKey(ReadOnlySpan<byte> pubKey) - => SetPublicKey(in NCUtil.AsPublicKey(pubKey)); - - /// <summary> - /// Assigns the public key used to encrypt the message as the - /// receiver of the message - /// </summary> - /// <param name="pubKey">The user's public key receiving the message</param> - /// <returns>The current instance for chaining</returns> - /// <exception cref="ArgumentException"></exception> - public NostrEncryptedMessage SetPublicKey(ref readonly NCPublicKey pubKey) - { - MemoryUtil.CloneStruct(in pubKey, ref _toKey); - return this; - } - - /// <summary> - /// Assigns the nonce to the message. Must be <see cref="NC_ENCRYPTION_NONCE_SIZE"/> - /// in length - /// </summary> - /// <param name="nonce">The nonce value to copy</param> - /// <returns>The current instance for chaining</returns> - /// <exception cref="ArgumentException"></exception> - public NostrEncryptedMessage SetNonce(ReadOnlySpan<byte> nonce) - { - MemoryUtil.CopyStruct(nonce, ref _nonce32); - return this; - } - - /// <summary> - /// Assigns a random nonce using the specified random source - /// </summary> - /// <param name="rng">The random source to genrate a random nonce from</param> - /// <returns>The current instance for chaining</returns> - public NostrEncryptedMessage SetRandomNonce(IRandomSource rng) - { - rng.GetRandomBytes(Nonce); - return this; - } - - /// <summary> - /// Encrypts the plaintext message and writes the encrypted message to the - /// specified buffer, along with a 32 byte mac of the message - /// </summary> - /// <param name="plaintext">The plaintext data to encrypt</param> - /// <param name="message">The message output buffer to write encrypted data to</param> - /// <param name="macOut32">A buffer to write the computed message mac to</param> - /// <returns>The number of bytes writtn to the message output buffer</returns> - /// <remarks> - /// The message buffer must be at-least the size of the output buffer, and it is not - /// initialized before the encryption operation. - /// </remarks> - /// <exception cref="ArgumentOutOfRangeException"></exception> - public int EncryptMessage(ReadOnlySpan<byte> plaintext, Span<byte> message, Span<byte> macOut32) - { - return Version switch - { - NC_ENC_VERSION_NIP44 => EncryptNip44(plaintext, message, macOut32), - _ => throw new NotSupportedException("NIP04 encryption is not supported"), - }; - } - - private int EncryptNip44(ReadOnlySpan<byte> plaintext, Span<byte> message, Span<byte> macOut32) - { - int payloadSize = GetOutputBufferSize(plaintext.Length); - - ArgumentOutOfRangeException.ThrowIfZero(plaintext.Length, nameof(plaintext)); - ArgumentOutOfRangeException.ThrowIfZero(message.Length, nameof(message)); - ArgumentOutOfRangeException.ThrowIfLessThan(message.Length, payloadSize, nameof(message)); - ArgumentOutOfRangeException.ThrowIfLessThan(macOut32.Length, NC_ENCRYPTION_MAC_SIZE, nameof(macOut32)); - - /* - * Alloc temp buffer to copy formatted payload to data to for the encryption - * operation. Encryption will write directly to the message buffer - */ - - using UnsafeMemoryHandle<byte> ptPayloadBuf = MemoryUtil.UnsafeAllocNearestPage<byte>(payloadSize, true); - using UnsafeMemoryHandle<byte> hmacKeyBuf = MemoryUtil.UnsafeAlloc<byte>(NC_HMAC_KEY_SIZE, true); - Debug.Assert(hmacKeyBuf.Length == NC_HMAC_KEY_SIZE); - - Nip44Util.FormatBuffer(plaintext, ptPayloadBuf.Span, false); - - library.EncryptNip44( - in _fromKey, - in _toKey, - in GetNonceRef(), - in ptPayloadBuf.GetReference(), - ref MemoryMarshal.GetReference(message), - (uint)payloadSize, //IMPORTANT: Format buffer will pad the buffer to the exact size - ref hmacKeyBuf.GetReference() //Must set the hmac key buffer - ); - - //Safe to clear the plain text copy buffer - MemoryUtil.InitializeBlock( - ref ptPayloadBuf.GetReference(), - ptPayloadBuf.GetIntLength() - ); - - - //Compute message mac, key should be set by the encryption operation - library.ComputeMac( - in hmacKeyBuf.GetReference(), - in MemoryMarshal.GetReference(message), - (uint)payloadSize, //Again set exact playload size - ref MemoryMarshal.GetReference(macOut32) - ); - - //Safe to clear the hmac key buffer - MemoryUtil.InitializeBlock( - ref hmacKeyBuf.GetReference(), - hmacKeyBuf.GetIntLength() - ); - - return payloadSize; - } - - private ref byte GetNonceRef() => ref Unsafe.As<Buffer32, byte>(ref _nonce32); - - protected override void Free() - { - //Zero all internal memory - MemoryUtil.ZeroStruct(ref _fromKey); - MemoryUtil.ZeroStruct(ref _toKey); - MemoryUtil.ZeroStruct(ref _nonce32); - } - - /// <summary> - /// Initializes a new <see cref="NostrEncryptedMessage"/> with the nip44 encryption - /// method. - /// </summary> - /// <param name="lib">The nostr crypto implementation instance to use</param> - /// <returns>The intialzied message instance</returns> - public static NostrEncryptedMessage CreateNip44Cipher(INostrCrypto lib) - => new(NCNip44EncryptionVersion.Instance, lib); - - - [StructLayout(LayoutKind.Sequential, Size = 32)] - unsafe struct Buffer32 - { - fixed byte value[32]; - } - } - -} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrMessageCipher.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrMessageCipher.cs new file mode 100644 index 0000000..0982d3a --- /dev/null +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/src/NostrMessageCipher.cs @@ -0,0 +1,424 @@ +// Copyright (C) 2024 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Authentication; + +using VNLib.Utils.Extensions; +using VNLib.Utils.Memory; + +using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; + +namespace VNLib.Utils.Cryptography.Noscrypt +{ + + public sealed class NostrMessageCipher(INostrCrypto lib, INostrEncryptionVersion version) : VnDisposeable + { + const int Nip44MaxMessageSize = 65603; + + private readonly INostrCrypto library = lib; + + private NCSecretKey _fromKey; + private NCPublicKey _toKey; + private Buffer32 _nonce32; + private Buffer32 _mac32; + + /// <summary> + /// The message encryption version used by this instance + /// </summary> + public uint Version { get; } = version.Version; + + /// <summary> + /// The message nonce created during encryption event + /// </summary> + public unsafe Span<byte> Nonce => MemoryMarshal.CreateSpan(ref GetNonceRef(), sizeof(Buffer32)); + + /// <summary> + /// The message MAC set during encryption, and required for decryption + /// </summary> + public unsafe Span<byte> Mac => MemoryMarshal.CreateSpan(ref GetMacRef(), sizeof(Buffer32)); + + /// <summary> + /// Gets the size of the buffer required to encrypt the specified data size + /// </summary> + /// <param name="dataSize">The size of the message raw plaintext message to send</param> + /// <returns>The minimum number of bytes required for message encryption output</returns> + /// <exception cref="NotSupportedException"></exception> + public int GetPayloadBufferSize(int dataSize) + => version.GetPayloadBufferSize(dataSize); + + /// <summary> + /// Gets the size of the buffer required to hold the full encrypted message data + /// for the encryption version used + /// </summary> + /// <param name="dataSize">The plaintext data size</param> + /// <returns>The estimated size of the output buffer</returns> + public int GetMessageBufferSize(int dataSize) + => version.GetMessageBufferSize(dataSize); + + /// <summary> + /// Sets the encryption secret key for the message + /// </summary> + /// <param name="secKey">The secret key buffer</param> + /// <returns>The current instance for chaining</returns> + /// <exception cref="ArgumentException"></exception> + public NostrMessageCipher SetSecretKey(ReadOnlySpan<byte> secKey) + => SetSecretKey(in NCUtil.AsSecretKey(secKey)); + + /// <summary> + /// Sets the encryption secret key for the message + /// </summary> + /// <param name="secKey">The secret key structure to copy</param> + /// <returns>The current instance for chaining</returns> + /// <exception cref="ArgumentException"></exception> + public NostrMessageCipher SetSecretKey(ref readonly NCSecretKey secKey) + { + MemoryUtil.CloneStruct(in secKey, ref _fromKey); + return this; + } + + /// <summary> + /// Assigns the public key used to encrypt the message as the + /// receiver of the message + /// </summary> + /// <param name="pubKey">The user's public key receiving the message</param> + /// <returns>The current instance for chaining</returns> + /// <exception cref="ArgumentException"></exception> + public NostrMessageCipher SetPublicKey(ReadOnlySpan<byte> pubKey) + => SetPublicKey(in NCUtil.AsPublicKey(pubKey)); + + /// <summary> + /// Assigns the public key used to encrypt the message as the + /// receiver of the message + /// </summary> + /// <param name="pubKey">The user's public key receiving the message</param> + /// <returns>The current instance for chaining</returns> + /// <exception cref="ArgumentException"></exception> + public NostrMessageCipher SetPublicKey(ref readonly NCPublicKey pubKey) + { + MemoryUtil.CloneStruct(in pubKey, ref _toKey); + return this; + } + + /// <summary> + /// Assigns the nonce to the message. Must be <see cref="NC_ENCRYPTION_NONCE_SIZE"/> + /// in length + /// </summary> + /// <param name="nonce">The nonce value to copy</param> + /// <returns>The current instance for chaining</returns> + /// <exception cref="ArgumentException"></exception> + public NostrMessageCipher SetNonce(ReadOnlySpan<byte> nonce) + { + MemoryUtil.CopyStruct(nonce, ref _nonce32); + return this; + } + + /// <summary> + /// Assigns a random nonce using the specified random source + /// </summary> + /// <param name="rng">The random source to genrate a random nonce from</param> + /// <returns>The current instance for chaining</returns> + public NostrMessageCipher SetRandomNonce(IRandomSource rng) + { + rng.GetRandomBytes(Nonce); + return this; + } + + /// <summary> + /// Configures a 32 byte mac for the message for nip44 decryption + /// </summary> + /// <param name="mac">The message mac</param> + /// <returns>The current instance for chaining</returns> + public NostrMessageCipher SetMac(ReadOnlySpan<byte> mac) + { + MemoryUtil.CopyStruct(mac, ref _mac32); + return this; + } + + /// <summary> + /// Decrypts a full nostr encrypted message and writes the plaintext + /// data to the output buffer + /// </summary> + /// <param name="message">The nostr message buffer to decrypt</param> + /// <param name="plaintext">The output plaintext buffer</param> + /// <returns>The number of bytes written the the plaintext buffer</returns> + /// <exception cref="FormatException"></exception> + /// <exception cref="NotSupportedException"></exception> + public int DecryptMessage(ReadOnlySpan<byte> message, Span<byte> plaintext) + { + return Version switch + { + NC_ENC_VERSION_NIP44 => DecryptNip44Message(message, plaintext), + _ => throw new NotSupportedException("NIP04 encryption is not supported"), + }; + } + + /// <summary> + /// Encrypts the plaintext message and writes the encrypted message to the + /// specified buffer. The output matches the format of the full nostr message + /// for the specified encryption version + /// </summary> + /// <param name="plaintext">The plaintext data to be encrypted</param> + /// <param name="message">The buffer to write the encrypted message data to</param> + /// <returns>The number of bytes written to the message buffer</returns> + /// <exception cref="NotSupportedException"></exception> + public int EncryptMessage(ReadOnlySpan<byte> plaintext, Span<byte> message) + { + return Version switch + { + NC_ENC_VERSION_NIP44 => EncryptNip44Message(plaintext, message), + _ => throw new NotSupportedException("NIP04 encryption is not supported"), + }; + } + + private int EncryptNip44Message(ReadOnlySpan<byte> plaintext, Span<byte> message) + { + int minRequiredOutSize = Nip44Util.CalcFinalBufferSize(plaintext.Length); + + ArgumentOutOfRangeException.ThrowIfZero(plaintext.Length, nameof(plaintext)); + ArgumentOutOfRangeException.ThrowIfLessThan(message.Length, minRequiredOutSize, nameof(message)); + + ForwardOnlyWriter<byte> messageWriter = new(message); + + // From spec -> concat(version, nonce, ciphertext, mac) + messageWriter.Append(0x02); // Version + messageWriter.Append<byte>(Nonce); // nonce + + //Encrypt plaintext and write directly the message buffer + int written = EncryptPayload(plaintext, messageWriter.Remaining); + + messageWriter.Advance(written); + + //Append the message mac, it was writen after the encryption operation + messageWriter.Append<byte>(Mac); + + return messageWriter.Written; + } + + /// <summary> + /// Encrypts the plaintext message and writes the encrypted message to the + /// specified buffer, along with a 32 byte mac of the message + /// </summary> + /// <param name="plaintext">The plaintext data to encrypt</param> + /// <param name="message">The message output buffer to write encrypted data to</param> + /// <param name="macOut32">A buffer to write the computed message mac to</param> + /// <returns>The number of bytes writtn to the message output buffer</returns> + /// <remarks> + /// The message buffer must be at-least the size of the output buffer, and it is not + /// initialized before the encryption operation. + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public int EncryptPayload(ReadOnlySpan<byte> plaintext, Span<byte> message) + { + return Version switch + { + NC_ENC_VERSION_NIP44 => EncryptNip44(plaintext, message), + _ => throw new NotSupportedException("NIP04 encryption is not supported"), + }; + } + + private int EncryptNip44(ReadOnlySpan<byte> plaintext, Span<byte> message) + { + int payloadSize = GetPayloadBufferSize(plaintext.Length); + + ArgumentOutOfRangeException.ThrowIfZero(plaintext.Length, nameof(plaintext)); + ArgumentOutOfRangeException.ThrowIfZero(message.Length, nameof(message)); + ArgumentOutOfRangeException.ThrowIfLessThan(message.Length, payloadSize, nameof(message)); + + /* + * Alloc temp buffer to copy formatted payload to data to for the encryption + * operation. Encryption will write directly to the message buffer + */ + + using UnsafeMemoryHandle<byte> ptPayloadBuf = MemoryUtil.UnsafeAllocNearestPage<byte>(payloadSize, true); + using UnsafeMemoryHandle<byte> hmacKeyBuf = MemoryUtil.UnsafeAlloc<byte>(NC_HMAC_KEY_SIZE, true); + + Debug.Assert(hmacKeyBuf.Length == NC_HMAC_KEY_SIZE); + + Nip44Util.FormatBuffer(plaintext, ptPayloadBuf.Span, false); + + library.EncryptNip44( + secretKey: in _fromKey, + publicKey: in _toKey, + nonce32: in GetNonceRef(), + plainText: in ptPayloadBuf.GetReference(), + cipherText: ref MemoryMarshal.GetReference(message), + size: (uint)payloadSize, //IMPORTANT: Format buffer will pad the buffer to the exact size + hmacKeyOut32: ref hmacKeyBuf.GetReference() //Must set the hmac key buffer + ); + + + //Compute message mac, key should be set by the encryption operation + library.ComputeMac( + hmacKey32: in hmacKeyBuf.GetReference(), + payload: in MemoryMarshal.GetReference(message), + payloadSize: (uint)payloadSize, //Again set exact playload size + hmacOut32: ref GetMacRef() + ); + + //Clear buffers + MemoryUtil.InitializeBlock(ref hmacKeyBuf.GetReference(), hmacKeyBuf.IntLength); + MemoryUtil.InitializeBlock(ref ptPayloadBuf.GetReference(), ptPayloadBuf.IntLength); + + return payloadSize; + } + + private int DecryptNip44Message(ReadOnlySpan<byte> message, Span<byte> plaintext) + { + //Full Nip44 messages must be at-least 99 bytes in length + ArgumentOutOfRangeException.ThrowIfLessThan(message.Length, 99, nameof(message)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(message.Length, Nip44MaxMessageSize, nameof(message)); + + //Message decoder used to get the nip44 message segments + Nip44MessageSegments msg = new(message); + + if (msg.Version != 0x02) + { + return 0; + } + + SetNonce(msg.Nonce); + SetMac(msg.Mac); + + //Temporary buffer to write decrypted plaintext data to + using UnsafeMemoryHandle<byte> plaintextBuffer = MemoryUtil.UnsafeAllocNearestPage<byte>(msg.Ciphertext.Length, true); + + int written = DecryptPayload(msg.Ciphertext, plaintextBuffer.Span); + + Span<byte> ptOut = plaintextBuffer.AsSpan(0, written); + + //Must check message bounds before returning a range + if (!Nip44Util.IsValidPlaintextMessage(ptOut)) + { + throw new FormatException("Plaintext data was not properly encrypted because it was not properly formatted or decryption failed"); + } + + Range msgRange = Nip44Util.GetPlaintextRange(ptOut); + Debug.Assert(msgRange.Start.Value > 0); + Debug.Assert(msgRange.End.Value > 0); + + int ptLength = msgRange.End.Value - msgRange.Start.Value; + + Debug.Assert(ptLength > 0); + + //Write the wrapped plaintext (unpadded) to the output plaintext buffer + MemoryUtil.Memmove( + src: in plaintextBuffer.GetReference(), + srcOffset: (uint)msgRange.Start.Value, + dst: ref MemoryMarshal.GetReference(plaintext), + dstOffset: 0, + elementCount: (uint)ptLength + ); + + return ptLength; + } + + /// <summary> + /// Decrypts a nostr encrypted message in it's full binary from. + /// </summary> + /// <param name="payload"></param> + /// <param name="plaintext"></param> + /// <returns>The number of bytes written to the output buffer, or an error code if an error occured during the encryption</returns> + /// <exception cref="NotSupportedException"></exception> + public int DecryptPayload(ReadOnlySpan<byte> payload, Span<byte> plaintext) + { + return Version switch + { + NC_ENC_VERSION_NIP44 => DecryptNip44Payload(payload, plaintext), + _ => throw new NotSupportedException("NIP04 encryption is not supported"), + }; + } + + private int DecryptNip44Payload(ReadOnlySpan<byte> message, Span<byte> plaintext) + { + ArgumentOutOfRangeException.ThrowIfZero(message.Length, nameof(message)); + ArgumentOutOfRangeException.ThrowIfZero(plaintext.Length, nameof(plaintext)); + + //Validate the incoming message for a nip44 message + ArgumentOutOfRangeException.ThrowIfLessThan(message.Length, 32, nameof(message)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(message.Length, Nip44MaxMessageSize, nameof(message)); + + //Plaintext buffer must be large enough to hold the decrypted message + ArgumentOutOfRangeException.ThrowIfLessThan(plaintext.Length, message.Length, nameof(plaintext)); + + bool macValid = library.VerifyMac( + in _fromKey, + in _toKey, + nonce32: in GetNonceRef(), + mac32: in GetMacRef(), + payload: ref MemoryMarshal.GetReference(message), + (uint)message.Length + ); + + if (!macValid) + { + throw new AuthenticationException("Message MAC is invalid"); + } + + library.DecryptNip44( + in _fromKey, + in _toKey, + nonce32: in GetNonceRef(), + cipherText: in MemoryMarshal.GetReference(message), + plainText: ref MemoryMarshal.GetReference(plaintext), + (uint)message.Length + ); + + //Return the number of bytes written to the output buffer + return message.Length; + } + + private unsafe ref byte GetNonceRef() + { + Debug.Assert(NC_ENCRYPTION_NONCE_SIZE == sizeof(Buffer32)); + return ref Unsafe.As<Buffer32, byte>(ref _nonce32); + } + + private unsafe ref byte GetMacRef() + { + Debug.Assert(NC_ENCRYPTION_MAC_SIZE == sizeof(Buffer32)); + return ref Unsafe.As<Buffer32, byte>(ref _mac32); + } + + protected override void Free() + { + //Zero all internal memory + MemoryUtil.ZeroStruct(ref _fromKey); + MemoryUtil.ZeroStruct(ref _toKey); + MemoryUtil.ZeroStruct(ref _nonce32); + MemoryUtil.ZeroStruct(ref _mac32); + } + + /// <summary> + /// Initializes a new <see cref="NostrMessageCipher"/> with the nip44 encryption + /// method. + /// </summary> + /// <param name="lib">The nostr crypto implementation instance to use</param> + /// <returns>The intialzied message instance</returns> + public static NostrMessageCipher CreateNip44Cipher(INostrCrypto lib) + => new(lib, NCNip44EncryptionVersion.Instance); + + + [StructLayout(LayoutKind.Sequential, Size = 32)] + unsafe struct Buffer32 + { + fixed byte value[32]; + } + } + +} diff --git a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/tests/LibNoscryptTests.cs b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/tests/LibNoscryptTests.cs index b55e9ff..73a62d9 100644 --- a/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/tests/LibNoscryptTests.cs +++ b/wrappers/dotnet/VNLib.Utils.Cryptography.Noscrypt/tests/LibNoscryptTests.cs @@ -3,10 +3,10 @@ using System; using System.Text; using System.Text.Json; -using System.Runtime.CompilerServices; using VNLib.Hashing; using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; namespace VNLib.Utils.Cryptography.Noscrypt.Tests { @@ -14,7 +14,7 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests public class LibNoscryptTests : IDisposable { - const string NoscryptLibWinDebug = @"../../../../../../../out/build/x64-debug/noscrypt.dll"; + const string NoscryptLibWinDebug = @"../../../../../../../out/build/x64-debug/Debug/noscrypt.dll"; //Keys generated using npx noskey package @@ -27,14 +27,14 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests const string Nip44VectorTestFile = "nip44.vectors.json"; #nullable disable - private LibNoscrypt _testLib; + private NoscryptLibrary _testLib; private JsonDocument _testVectors; #nullable enable [TestInitialize] public void Initialize() { - _testLib = LibNoscrypt.Load(NoscryptLibWinDebug); + _testLib = NoscryptLibrary.Load(NoscryptLibWinDebug); _testVectors = JsonDocument.Parse(File.ReadAllText(Nip44VectorTestFile)); } @@ -126,11 +126,11 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests //noThrow (its a bad sec key but it should not throw) crypto.ValidateSecretKey(ref secKey); - Assert.ThrowsException<ArgumentNullException>(() => crypto.ValidateSecretKey(ref Unsafe.NullRef<NCSecretKey>())); + Assert.ThrowsException<ArgumentNullException>(() => crypto.ValidateSecretKey(ref NCSecretKey.NullRef)); //public key - Assert.ThrowsException<ArgumentNullException>(() => crypto.GetPublicKey(ref Unsafe.NullRef<NCSecretKey>(), ref pubKey)); - Assert.ThrowsException<ArgumentNullException>(() => crypto.GetPublicKey(in secKey, ref Unsafe.NullRef<NCPublicKey>())); + Assert.ThrowsException<ArgumentNullException>(() => crypto.GetPublicKey(ref NCSecretKey.NullRef, ref pubKey)); + Assert.ThrowsException<ArgumentNullException>(() => crypto.GetPublicKey(in secKey, ref NCPublicKey.NullRef)); } [TestMethod()] @@ -159,48 +159,41 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests { using NostrCrypto nc = _testLib.InitializeCrypto(MemoryUtil.Shared, RandomHash.GetRandomBytes(32)); - Span<byte> macOut32 = stackalloc byte[32]; + using NostrMessageCipher cipher = NostrMessageCipher.CreateNip44Cipher(nc); foreach (EncryptionVector v in GetEncryptionVectors()) - { - using NostrEncryptedMessage msg = NostrEncryptedMessage.CreateNip44Cipher(nc); + { ReadOnlySpan<byte> secKey1 = Convert.FromHexString(v.sec1); ReadOnlySpan<byte> secKey2 = Convert.FromHexString(v.sec2); ReadOnlySpan<byte> plainText = Encoding.UTF8.GetBytes(v.plaintext); ReadOnlySpan<byte> nonce = Convert.FromHexString(v.nonce); ReadOnlySpan<byte> message = Convert.FromBase64String(v.payload); - ReadOnlySpan<byte> conversationKey = Convert.FromHexString(v.conversation_key); - - Nip44Message nip44Message = new(message); - - int ptSize = msg.GetOutputBufferSize(plainText.Length); - - Assert.AreEqual<int>(nip44Message.Ciphertext.Length, ptSize); - Assert.AreEqual<byte>(nip44Message.Version, 0x02); - Assert.IsTrue(nonce.SequenceEqual(nip44Message.Nonce)); NCPublicKey pub2; //Recover public keys nc.GetPublicKey(in NCUtil.AsSecretKey(secKey2), ref pub2); - Span<byte> actualCiphertext = new byte[ptSize + 32]; + int outBufferSize = cipher.GetMessageBufferSize(plainText.Length); + + Span<byte> encryptedNote = new byte[outBufferSize]; - msg.SetSecretKey(secKey1) + cipher.SetSecretKey(secKey1) .SetPublicKey(in pub2) .SetNonce(nonce); - int written = msg.EncryptMessage(plainText, actualCiphertext, macOut32); + int written = cipher.EncryptMessage(plainText, encryptedNote); + Assert.IsTrue(written > 0); - actualCiphertext = actualCiphertext[..written]; + encryptedNote = encryptedNote[..written]; //Make sure the cipher text matches the expected payload - if (!actualCiphertext.SequenceEqual(nip44Message.Ciphertext)) + if (!encryptedNote.SequenceEqual(message)) { Console.WriteLine($"Input data: {v.plaintext}"); - Console.WriteLine($" \n{Convert.ToHexString(actualCiphertext)}\n{Convert.ToHexString(nip44Message.Ciphertext)}"); - Assert.Fail($"Cipher text does not match expected payload"); + Console.WriteLine($" \n{Convert.ToHexString(encryptedNote)}\n{Convert.ToHexString(message)}"); + Assert.Fail($"Cipher text does not match expected message"); } } } @@ -216,7 +209,7 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests ReadOnlySpan<byte> secKey2 = Convert.FromHexString(v.sec2); ReadOnlySpan<byte> message = Convert.FromBase64String(v.payload); - Nip44Message nip44Message = new(message); + Nip44MessageSegments nip44Message = new(message); Assert.AreEqual<byte>(nip44Message.Version, 0x02); NCPublicKey pub2; @@ -277,9 +270,11 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests [TestMethod()] public void CorrectDecryptionTest() { - using NostrCrypto nc = _testLib.InitializeCrypto(MemoryUtil.Shared, RandomHash.GetRandomBytes(32)); + using NostrCrypto nc = _testLib.InitializeCrypto(MemoryUtil.Shared, NcFallbackRandom.Shared); - Span<byte> hmacKeyOut = stackalloc byte[LibNoscrypt.NC_HMAC_KEY_SIZE]; + using NostrMessageCipher msgCipher = NostrMessageCipher.CreateNip44Cipher(nc); + + using IMemoryHandle<byte> ptBuffer = MemoryUtil.SafeAllocNearestPage(1200, false); foreach (EncryptionVector vector in GetEncryptionVectors()) { @@ -289,38 +284,30 @@ namespace VNLib.Utils.Cryptography.Noscrypt.Tests ReadOnlySpan<byte> nonce = Convert.FromHexString(vector.nonce); ReadOnlySpan<byte> message = Convert.FromBase64String(vector.payload); - Nip44Message nip44Message = new(message); - - Assert.IsTrue(nip44Message.Version == 0x02); - Assert.IsTrue(nonce.SequenceEqual(nip44Message.Nonce)); - - NCPublicKey pub1; + NCPublicKey pub1 = default; //Recover public keys nc.GetPublicKey(in NCUtil.AsSecretKey(secKey1), ref pub1); - - Span<byte> plaintextOut = new byte[ Nip44Util.CalcBufferSize(expectedPt.Length) ]; - - Assert.IsTrue(nip44Message.Ciphertext.Length == plaintextOut.Length); - - /* - * Decrypting messages requires the public key of the sender - * and the secret key of the receiver - */ - nc.Decrypt( - in NCUtil.AsSecretKey(secKey2), - in pub1, - nip44Message.Nonce, - nip44Message.Ciphertext, - plaintextOut - ); - ReadOnlySpan<byte> actualPt = Nip44Util.GetPlaintextMessage(plaintextOut); + msgCipher.SetPublicKey(in pub1) + .SetSecretKey(secKey2); + + int outLen = msgCipher.DecryptMessage(message, ptBuffer.Span); + + Assert.IsTrue(outLen > 0); - Assert.AreEqual<int>(expectedPt.Length, actualPt.Length); - Assert.IsTrue(actualPt.SequenceEqual(expectedPt)); + Span<byte> plaintext = ptBuffer.AsSpan(0, outLen); - MemoryUtil.InitializeBlock(hmacKeyOut); + if (!plaintext.SequenceEqual(expectedPt)) + { + Console.WriteLine($"Input data: {vector.plaintext}"); + Console.WriteLine($" \n{Convert.ToHexString(plaintext)}\n{Convert.ToHexString(expectedPt)}"); + Assert.Fail("Decrypted data does not match expected plaintext"); + } + else + { + Assert.IsTrue(nonce.SequenceEqual(msgCipher.Nonce)); + } } } |