using System; using System.Buffers; using System.Buffers.Binary; using System.Threading; using Lidgren.Network; using SpaceWizards.Sodium; namespace Robust.Shared.Network; internal sealed class NetEncryption { public const int EncryptionOverhead = sizeof(ulong) + CryptoAeadXChaCha20Poly1305Ietf.AddBytes; // Use a counter for nonces. The counter is 64-bit, I will be impressed if you ever manage to run it out. // 64-bit counter (incl over the wire) is fine, don't need the whole 192-bit. // Server starts at 0, client starts at 1, increment by two. // This means server and client never use eachother's nonces (one side odd, one side even). // Keep in mind, our keys are only valid for one session. private ulong _nonce; private readonly byte[] _key; public NetEncryption(byte[] key, bool isServer) { if (key.Length != CryptoAeadXChaCha20Poly1305Ietf.KeyBytes) throw new ArgumentException("Key is of wrong size!"); _nonce = isServer ? 0ul : 1ul; _key = key; } public unsafe void Encrypt(NetOutgoingMessage message) { var nonce = Interlocked.Add(ref _nonce, 2); var lengthBytes = message.LengthBytes; var encryptedSize = lengthBytes + EncryptionOverhead; var data = message.Data.AsSpan(0, lengthBytes); Span plaintext; Span ciphertext; byte[]? returnPool = null; if (message.Data.Length >= encryptedSize) { // Since we have enough space in the existing message data, // we copy plaintext to an ArrayPool buffer and write ciphertext into existing message. // This avoids an allocation at the cost of an extra copy operation. returnPool = ArrayPool.Shared.Rent(lengthBytes); plaintext = returnPool.AsSpan(0, lengthBytes); data.CopyTo(plaintext); ciphertext = message.Data.AsSpan(0, encryptedSize); } else { // Otherwise, an allocation is unavoidable, // so we swap the data buffer in the message with a fresh allocation and don't do an extra copy of the data. plaintext = data; ciphertext = message.Data = new byte[encryptedSize]; } Span nonceData = stackalloc byte[CryptoAeadXChaCha20Poly1305Ietf.NoncePublicBytes]; nonceData.Fill(0); BinaryPrimitives.WriteUInt64LittleEndian(nonceData, nonce); BinaryPrimitives.WriteUInt64LittleEndian(ciphertext, nonce); CryptoAeadXChaCha20Poly1305Ietf.Encrypt( // ciphertext ciphertext[sizeof(ulong)..], out _, // plaintext plaintext, // additional data (unused) ReadOnlySpan.Empty, // nonce nonceData, // key _key); message.LengthBytes = encryptedSize; if (returnPool != null) ArrayPool.Shared.Return(returnPool); } /// /// Attempts to decrypt an incoming network message, falliably. /// /// The message to decrypt in-place. This will be mutated with the decrypted results. /// Whether the operation was successful. If this fails, you likely want to drop the connection. public unsafe bool TryDecrypt(NetIncomingMessage message) { // Minimum possible size a message can be is the nonce + 16 bytes of message. // So we immediately bail on anything smaller. if (message.LengthBytes < sizeof(ulong) + CryptoAeadXChaCha20Poly1305Ietf.AddBytes) return false; var nonce = message.ReadUInt64(); var cipherText = message.Data.AsSpan(sizeof(ulong), message.LengthBytes - sizeof(ulong)); var buffer = ArrayPool.Shared.Rent(cipherText.Length); cipherText.CopyTo(buffer); Span nonceData = stackalloc byte[CryptoAeadXChaCha20Poly1305Ietf.NoncePublicBytes]; nonceData.Fill(0); BinaryPrimitives.WriteUInt64LittleEndian(nonceData, nonce); var result = CryptoAeadXChaCha20Poly1305Ietf.Decrypt( // plaintext message.Data, out var messageLength, // ciphertext buffer.AsSpan(0, cipherText.Length), // additional data (unused) ReadOnlySpan.Empty, // nonce nonceData, // key _key); message.Position = 0; message.LengthBytes = messageLength; ArrayPool.Shared.Return(buffer); return result; } }