using System.Buffers.Binary; using VoiceCat.Crypto; using VoiceCat.Protocol; namespace VoiceCat.Tests; public class MediaTests { private static readonly byte[] Key = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray(); private static readonly VoiceFrameHeader Header = new(MediaFrameType.Voice, VoiceFrameFlags.Marker, 0, 0xcafebabe, 0, 960); [Theory] [InlineData(false)] [InlineData(true)] public void BothBackendsProduceIdenticalPackets(bool managed) { using var sender = new MediaEncryptor(Key, managed); using var receiver = new MediaDecryptor(Key, !managed); byte[] plaintext = Enumerable.Range(0, 100).Select(i => (byte)i).ToArray(); byte[] packet = Seal(sender, plaintext); byte[] output = new byte[plaintext.Length]; Assert.True(receiver.TryDecrypt(packet, output, out var header, out int written)); Assert.Equal(Header, header); Assert.Equal(plaintext.Length, written); Assert.Equal(plaintext, output); Assert.False(receiver.TryDecrypt(packet, output, out _, out written)); Assert.Equal(0, written); } [Theory] [InlineData(false)] [InlineData(true)] public void ForgedCounterDoesNotPoisonReplayWindow(bool managed) { using var sender = new MediaEncryptor(Key, managed); using var receiver = new MediaDecryptor(Key, managed); byte[] output = new byte[8]; Assert.True(receiver.TryDecrypt(Seal(sender, new byte[8]), output, out _, out _)); byte[] packet = Seal(sender, new byte[8]); byte[] forged = (byte[])packet.Clone(); BinaryPrimitives.WriteUInt64BigEndian(forged.AsSpan(8), ulong.MaxValue); Array.Fill(output, (byte)0xaa); Assert.False(receiver.TryDecrypt(forged, output, out var header, out int written)); Assert.Equal(default, header); Assert.Equal(0, written); Assert.All(output, value => Assert.Equal(0, value)); Assert.True(receiver.TryDecrypt(packet, output, out _, out _)); Assert.True(receiver.TryDecrypt(Seal(sender, new byte[8]), output, out _, out _)); } [Theory] [InlineData(false)] [InlineData(true)] public void TamperingEveryPacketRegionFailsAuthentication(bool managed) { using var sender = new MediaEncryptor(Key, managed); byte[] packet = Seal(sender, new byte[80]); for (int i = 0; i < packet.Length; i++) { using var receiver = new MediaDecryptor(Key, managed); byte[] tampered = (byte[])packet.Clone(); tampered[i] ^= 0x80; Assert.False(receiver.TryDecrypt(tampered, new byte[80], out _, out _)); Assert.True(receiver.TryDecrypt(packet, new byte[80], out _, out _)); } } [Theory] [InlineData(false)] [InlineData(true)] public void ReplayWindowAcceptsReorderingAndRejectsOldPackets(bool managed) { using var sender = new MediaEncryptor(Key, managed); using var receiver = new MediaDecryptor(Key, managed); var packets = Enumerable.Range(0, 130).Select(_ => Seal(sender, new byte[1])).ToArray(); byte[] output = new byte[1]; Assert.True(receiver.TryDecrypt(packets[64], output, out _, out _)); Assert.False(receiver.TryDecrypt(packets[0], output, out _, out _)); Assert.True(receiver.TryDecrypt(packets[1], output, out _, out _)); Assert.False(receiver.TryDecrypt(packets[1], output, out _, out _)); Assert.True(receiver.TryDecrypt(packets[63], output, out _, out _)); Assert.True(receiver.TryDecrypt(packets[129], output, out _, out _)); Assert.False(receiver.TryDecrypt(packets[64], output, out _, out _)); Assert.True(receiver.TryDecrypt(packets[128], output, out _, out _)); } [Theory] [InlineData(false)] [InlineData(true)] public void CounterCrossesOldSixteenBitBoundary(bool managed) { using var sender = new MediaEncryptor(Key, managed, 65534); using var receiver = new MediaDecryptor(Key, managed); for (ulong sequence = 65534; sequence < 65540; sequence++) { Assert.True(receiver.TryDecrypt(Seal(sender, new byte[1]), new byte[1], out var header, out _)); Assert.Equal(sequence, header.Sequence); } } [Theory] [InlineData(false)] [InlineData(true)] public void InterleavedRelayUsesRecipientCounter(bool managed) { byte[] otherKey = Enumerable.Repeat((byte)42, 32).ToArray(); using var a = new MediaEncryptor(Key, managed); using var b = new MediaEncryptor(otherKey, managed); using var receiveA = new MediaDecryptor(Key, managed); using var receiveB = new MediaDecryptor(otherKey, managed); using var relay = new MediaEncryptor(Key, managed); using var listener = new MediaDecryptor(Key, managed); byte[] plaintext = [1, 2, 3]; byte[] decoded = new byte[3]; for (int i = 0; i < 16; i++) { var sender = i % 2 == 0 ? a : b; var receiver = i % 2 == 0 ? receiveA : receiveB; Assert.True(receiver.TryDecrypt(Seal(sender, plaintext), decoded, out var header, out _)); byte[] packet = new byte[39]; relay.Encrypt(header, decoded, packet); Assert.True(listener.TryDecrypt(packet, decoded, out var relayedHeader, out _)); Assert.Equal((ulong)i, relayedHeader.Sequence); Assert.Equal(plaintext, decoded); } } [Theory] [InlineData(false)] [InlineData(true)] public void EmptyPayloadAndLargeCountersWork(bool managed) { using var sender = new MediaEncryptor(Key, managed, ulong.MaxValue - 1); using var receiver = new MediaDecryptor(Key, managed); var packet = Seal(sender, []); Assert.True(receiver.TryDecrypt(packet, [], out var header, out int written)); Assert.Equal(ulong.MaxValue - 1, header.Sequence); Assert.Equal(0, written); Assert.Throws(() => Seal(sender, [])); } [Fact] public void InvalidArgumentsAndDisposedInstancesAreRejected() { Assert.Throws(() => new MediaEncryptor(new byte[31])); using var sender = new MediaEncryptor(Key); using var receiver = new MediaDecryptor(Key); Assert.Throws(() => sender.Encrypt(Header, new byte[1], new byte[36])); byte[] packet = Seal(sender, new byte[8]); Assert.True(receiver.TryDecrypt(packet, new byte[8], out var header, out _)); Assert.Equal(0UL, header.Sequence); Assert.False(receiver.TryDecrypt(new byte[35], [], out _, out _)); Assert.Throws(() => receiver.TryDecrypt(packet, [], out _, out _)); sender.Dispose(); receiver.Dispose(); Assert.Throws(() => Seal(sender, [])); Assert.Throws(() => receiver.TryDecrypt(packet, new byte[8], out _, out _)); } private static byte[] Seal(MediaEncryptor sender, byte[] plaintext) { byte[] packet = new byte[VoiceFrameHeader.Size + plaintext.Length + MediaEncryptor.TagSize]; Assert.Equal(packet.Length, sender.Encrypt(Header, plaintext, packet)); return packet; } }