Editor/Core/AeadFraming.cs
using System;
using System.Buffers.Binary;
using System.Security.Cryptography;

namespace TeamCreate;

public sealed class AeadFraming
{
    public const int KeyBytes = 32;

    public const int NonceBytes = 12;

    public const int TagBytes = 16;

    public const int FrameOverhead = 28;

    private static readonly byte[] InitiatorTag = new byte[4] { 67, 84, 45, 65 };

    private static readonly byte[] ResponderTag = new byte[4] { 67, 84, 45, 66 };

    private readonly byte[] key;

    private readonly byte[] sealTag;

    private readonly byte[] openTag;

    private readonly object gate = new object();

    private long counter = 1L;

    // The counter the next opened frame must carry. Sealing numbers frames 1, 2, 3 without gaps,
    // and a stream (TCP, or the relay's ordered byte pipe) delivers them in that order, so anything
    // else on the wire is a replayed, dropped, duplicated or reordered frame. Accepting any valid
    // tag was fine while only the two endpoints could touch the stream; a relay sits in the middle
    // and could otherwise re-inject or withhold frames without breaking a single authentication tag.
    private long expectedOpenCounter = 1L;

    private long bytesSealed;

    public const string SequenceFailure = "A sealed frame arrived out of sequence; the stream was replayed, reordered or had frames removed.";

    public long BytesSealed => bytesSealed;

    public AeadFraming(byte[] key, bool initiator = false)
    {
        if (key == null || key.Length != 32)
        {
            throw new ArgumentOutOfRangeException("key", "An AEAD session key must be exactly 32 bytes.");
        }
        this.key = (byte[])key.Clone();
        sealTag = (initiator ? InitiatorTag : ResponderTag);
        openTag = (initiator ? ResponderTag : InitiatorTag);
    }

    internal void SetCounterForTests(long value)
    {
        if (value < 1)
        {
            throw new ArgumentOutOfRangeException("value", "The framing counter starts at 1 and never runs backwards.");
        }
        lock (gate)
        {
            counter = value;
        }
    }

    public byte[] Seal(ReadOnlySpan<byte> plaintext)
    {
        long value;
        lock (gate)
        {
            if (counter >= long.MaxValue)
            {
                throw new InvalidOperationException("The framing counter is exhausted; reconnect with a fresh session key instead of reusing a nonce.");
            }
            value = counter++;
            bytesSealed += plaintext.Length;
        }
        byte[] array = new byte[28 + plaintext.Length];
        Span<byte> span = array.AsSpan(0, 12);
        sealTag.CopyTo(span);
        BinaryPrimitives.WriteInt64BigEndian(span.Slice(4), value);
        using AesGcm aesGcm = new AesGcm(key, 16);
        aesGcm.Encrypt(span, plaintext, array.AsSpan(12, plaintext.Length), array.AsSpan(12 + plaintext.Length, 16));
        return array;
    }

    public byte[] Open(ReadOnlySpan<byte> sealedFrame)
    {
        if (sealedFrame.Length < 28)
        {
            throw new CryptographicException("The sealed frame is too short to hold a nonce and an authentication tag.");
        }
        ReadOnlySpan<byte> nonce = sealedFrame.Slice(0, 12);
        if (!nonce.Slice(0, 4).SequenceEqual(openTag))
        {
            throw new CryptographicException("The sealed frame carries the opposite direction tag; both ends of a session must use opposite initiator flags.");
        }
        long counterOnWire = BinaryPrimitives.ReadInt64BigEndian(nonce.Slice(4));
        lock (gate)
        {
            if (counterOnWire != expectedOpenCounter)
            {
                throw new CryptographicException(SequenceFailure);
            }
        }
        byte[] array = new byte[sealedFrame.Length - 28];
        using AesGcm aesGcm = new AesGcm(key, 16);
        aesGcm.Decrypt(nonce, sealedFrame.Slice(12, array.Length), sealedFrame.Slice(12 + array.Length), array);
        // Advance only after the tag verified: a forged frame must not be able to move the window.
        lock (gate)
        {
            expectedOpenCounter = counterOnWire + 1;
        }
        return array;
    }
}