// 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.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Authentication; 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; /// /// The message encryption version used by this instance /// public uint Version { get; } = version.Version; /// /// The message nonce created during encryption event /// public unsafe Span Nonce => MemoryMarshal.CreateSpan(ref GetNonceRef(), sizeof(Buffer32)); /// /// The message MAC set during encryption, and required for decryption /// public unsafe Span Mac => MemoryMarshal.CreateSpan(ref GetMacRef(), sizeof(Buffer32)); /// /// Gets the size of the buffer required to encrypt the specified data size /// /// The size of the message raw plaintext message to send /// The minimum number of bytes required for message encryption output /// public int GetPayloadBufferSize(int dataSize) => version.GetPayloadBufferSize(dataSize); /// /// Gets the size of the buffer required to hold the full encrypted message data /// for the encryption version used /// /// The plaintext data size /// The estimated size of the output buffer public int GetMessageBufferSize(int dataSize) => version.GetMessageBufferSize(dataSize); /// /// Sets the encryption secret key for the message /// /// The secret key buffer /// The current instance for chaining /// public NostrMessageCipher SetSecretKey(ReadOnlySpan secKey) => SetSecretKey(in NCUtil.AsSecretKey(secKey)); /// /// Sets the encryption secret key for the message /// /// The secret key structure to copy /// The current instance for chaining /// public NostrMessageCipher SetSecretKey(ref readonly NCSecretKey secKey) { MemoryUtil.CloneStruct(in secKey, ref _fromKey); return this; } /// /// Assigns the public key used to encrypt the message as the /// receiver of the message /// /// The user's public key receiving the message /// The current instance for chaining /// public NostrMessageCipher SetPublicKey(ReadOnlySpan pubKey) => SetPublicKey(in NCUtil.AsPublicKey(pubKey)); /// /// Assigns the public key used to encrypt the message as the /// receiver of the message /// /// The user's public key receiving the message /// The current instance for chaining /// public NostrMessageCipher SetPublicKey(ref readonly NCPublicKey pubKey) { MemoryUtil.CloneStruct(in pubKey, ref _toKey); return this; } /// /// Assigns the nonce to the message. Must be /// in length /// /// The nonce value to copy /// The current instance for chaining /// public NostrMessageCipher SetNonce(ReadOnlySpan nonce) { MemoryUtil.CopyStruct(nonce, ref _nonce32); return this; } /// /// Assigns a random nonce using the specified random source /// /// The random source to genrate a random nonce from /// The current instance for chaining public NostrMessageCipher SetRandomNonce(IRandomSource rng) { rng.GetRandomBytes(Nonce); return this; } /// /// Configures a 32 byte mac for the message for nip44 decryption /// /// The message mac /// The current instance for chaining public NostrMessageCipher SetMac(ReadOnlySpan mac) { MemoryUtil.CopyStruct(mac, ref _mac32); return this; } /// /// Decrypts a full nostr encrypted message and writes the plaintext /// data to the output buffer /// /// The nostr message buffer to decrypt /// The output plaintext buffer /// The number of bytes written the the plaintext buffer /// /// public int DecryptMessage(ReadOnlySpan message, Span plaintext) { return Version switch { NC_ENC_VERSION_NIP44 => DecryptNip44Message(message, plaintext), _ => throw new NotSupportedException("NIP04 encryption is not supported"), }; } /// /// 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 /// /// The plaintext data to be encrypted /// The buffer to write the encrypted message data to /// The number of bytes written to the message buffer /// public int EncryptMessage(ReadOnlySpan plaintext, Span message) { return Version switch { NC_ENC_VERSION_NIP44 => EncryptNip44Message(plaintext, message), _ => throw new NotSupportedException("NIP04 encryption is not supported"), }; } private int EncryptNip44Message(ReadOnlySpan plaintext, Span message) { int minRequiredOutSize = Nip44Util.CalcFinalBufferSize(plaintext.Length); ArgumentOutOfRangeException.ThrowIfZero(plaintext.Length, nameof(plaintext)); ArgumentOutOfRangeException.ThrowIfLessThan(message.Length, minRequiredOutSize, nameof(message)); ForwardOnlyWriter messageWriter = new(message); // From spec -> concat(version, nonce, ciphertext, mac) messageWriter.Append(0x02); // Version messageWriter.Append(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(Mac); return messageWriter.Written; } /// /// Encrypts the plaintext message and writes the encrypted message to the /// specified buffer, along with a 32 byte mac of the message /// /// The plaintext data to encrypt /// The message output buffer to write encrypted data to /// A buffer to write the computed message mac to /// The number of bytes writtn to the message output buffer /// /// The message buffer must be at-least the size of the output buffer, and it is not /// initialized before the encryption operation. /// /// public int EncryptPayload(ReadOnlySpan plaintext, Span message) { return Version switch { NC_ENC_VERSION_NIP44 => EncryptNip44(plaintext, message), _ => throw new NotSupportedException("NIP04 encryption is not supported"), }; } private int EncryptNip44(ReadOnlySpan plaintext, Span 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 ptPayloadBuf = MemoryUtil.UnsafeAllocNearestPage(payloadSize, true); using UnsafeMemoryHandle hmacKeyBuf = MemoryUtil.UnsafeAlloc(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 message, Span 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 plaintextBuffer = MemoryUtil.UnsafeAllocNearestPage(msg.Ciphertext.Length, true); int written = DecryptPayload(msg.Ciphertext, plaintextBuffer.Span); Span 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; } /// /// Decrypts a nostr encrypted message in it's full binary from. /// /// /// /// The number of bytes written to the output buffer, or an error code if an error occured during the encryption /// public int DecryptPayload(ReadOnlySpan payload, Span plaintext) { return Version switch { NC_ENC_VERSION_NIP44 => DecryptNip44Payload(payload, plaintext), _ => throw new NotSupportedException("NIP04 encryption is not supported"), }; } private int DecryptNip44Payload(ReadOnlySpan message, Span 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(ref _nonce32); } private unsafe ref byte GetMacRef() { Debug.Assert(NC_ENCRYPTION_MAC_SIZE == sizeof(Buffer32)); return ref Unsafe.As(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); } /// /// Initializes a new with the nip44 encryption /// method. /// /// The nostr crypto implementation instance to use /// The intialzied message instance public static NostrMessageCipher CreateNip44Cipher(INostrCrypto lib) => new(lib, NCNip44EncryptionVersion.Instance); [StructLayout(LayoutKind.Sequential, Size = 32)] unsafe struct Buffer32 { fixed byte value[32]; } } }