// 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 . using System; using System.Buffers.Binary; using System.Runtime.InteropServices; using VNLib.Utils.Memory; using static VNLib.Utils.Cryptography.Noscrypt.NoscryptLibrary; namespace VNLib.Utils.Cryptography.Noscrypt { /// /// Provides a set of utility methods for working with the Noscrypt library /// public static class Nip44Util { /// /// Calculates the required NIP44 encryption buffer size for /// the specified input data size /// /// The size (in bytes) of the encoded data to encrypt /// The exact size of the padded buffer output public static int CalcBufferSize(int dataSize) { /* * Taken from https://github.com/nostr-protocol/nips/blob/master/44.md * * Not gonna lie, kinda dumb branches. I guess they want to save space * with really tiny messages... Dunno, but whatever RTFM */ //Min message size is 32 bytes int minSize = Math.Max(dataSize, 32); //find the next power of 2 that will fit the min size int nexPower = 1 << ((int)Math.Log2(minSize - 1)) + 1; int chunk = nexPower <= 256 ? 32 : nexPower / 8; return (chunk * ((int)Math.Floor((double)((minSize - 1) / chunk)) + 1)) + sizeof(ushort); } /// /// Calculates the final buffer size required to hold the encrypted data /// /// The size (in bytes) of plaintext data to encrypt /// The number of bytes required to store the final nip44 message public static int CalcFinalBufferSize(int dataSize) { /* version + nonce + payload + mac */ return CalcBufferSize(dataSize) + NC_ENCRYPTION_NONCE_SIZE + NC_ENCRYPTION_MAC_SIZE + 1; } /// /// Formats the plaintext data into a buffer that can be properly encrypted. /// The output buffer must be zeroed, or can be zeroed using the /// parameter. Use /// to determine the required output buffer size. /// /// A buffer containing plaintext data to copy to the output /// The output data buffer to format /// A value that indicates if the buffer should be zeroed before use public static void FormatBuffer(ReadOnlySpan plaintextData, Span output, bool zeroOutput) { //First zero out the buffer if (zeroOutput) { MemoryUtil.InitializeBlock(output); } //Make sure the output buffer is large enough so we dont overrun it ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, plaintextData.Length + sizeof(ushort), nameof(output)); //Write the data size to the first 2 bytes ushort dataSize = (ushort)plaintextData.Length; BinaryPrimitives.WriteUInt16BigEndian(output, dataSize); //Copy the plaintext data to the output buffer after the data size MemoryUtil.Memmove( 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 payloadBuffer, byte version, ReadOnlySpan mac, Span 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 GetNonceFromPayload(ReadOnlySpan message) { //The nonce is 32 bytes following the 1st byte version number of the message return message.Slice(1, NC_ENCRYPTION_NONCE_SIZE); } public static ReadOnlySpan GetCiphertextFromPayload(ReadOnlySpan message) { //Message is between the nonce and the trailing mac int payloadSize = message.Length - (1 + NC_ENCRYPTION_NONCE_SIZE + NC_ENCRYPTION_MAC_SIZE); return message.Slice(1 + NC_ENCRYPTION_NONCE_SIZE, payloadSize); } public static ReadOnlySpan GetMacFromPayload(ReadOnlySpan message) { //The mac is the last 32 bytes of the message return message[^NC_ENCRYPTION_MAC_SIZE..]; } public static ReadOnlySpan GetNonceAndCiphertext(ReadOnlySpan message) { //The nonce is 32 bytes following the 1st byte version number of the message return message.Slice(1, NC_ENCRYPTION_NONCE_SIZE + GetCiphertextFromPayload(message).Length); } public static byte GetMessageVersion(ReadOnlySpan message) { //The first byte is the message version return message[0]; } public static ReadOnlySpan GetPlaintextMessage(ReadOnlySpan plaintextPayload) { ushort ptLength = BinaryPrimitives.ReadUInt16BigEndian(plaintextPayload); return plaintextPayload.Slice(sizeof(ushort), ptLength); } public static bool IsValidPlaintextMessage(ReadOnlySpan plaintextPayload) { ushort ptLength = BinaryPrimitives.ReadUInt16BigEndian(plaintextPayload); return ptLength == plaintextPayload.Length - sizeof(ushort); } public static Range GetPlaintextRange(ReadOnlySpan plaintextPayload) { ushort ptLength = BinaryPrimitives.ReadUInt16BigEndian(plaintextPayload); return new Range(sizeof(ushort), ptLength); } } }