using System; using System.Security.Cryptography; using System.Text; namespace axXez.TS3.Protocol { internal static class VoiceCrypto { public const int MacLen = 8; public const uint InitVersion = 1566914096; // "TS3INIT1" — MAC written on init handshake packets public static readonly byte[] InitMac = Encoding.ASCII.GetBytes("TS3INIT1"); // "c:\windows\system\firewall32.cpl" split into key(16) + nonce(16) public static readonly byte[] DummyKey = Encoding.ASCII.GetBytes("c:\\windows\\syste"); public static readonly byte[] DummyNonce = Encoding.ASCII.GetBytes("m\\firewall32.cpl"); private const int BlockSize = 16; private static readonly byte[] ZeroBlock = new byte[BlockSize]; private const byte Rb = 0x87; public static byte[] Cmac(byte[] key, byte[] data) { using var aes = Aes.Create(); aes.Key = key; byte[] L = new byte[BlockSize]; aes.EncryptEcb(ZeroBlock.AsSpan(), L.AsSpan(), PaddingMode.None); byte[] K1 = DoubleBlock(L); byte[] K2 = DoubleBlock(K1); int n = data.Length; int numBlocks = n == 0 ? 1 : (n + BlockSize - 1) / BlockSize; bool lastComplete = n > 0 && n % BlockSize == 0; byte[] x = new byte[BlockSize]; for (int i = 0; i < numBlocks - 1; i++) { for (int j = 0; j < BlockSize; j++) x[j] ^= data[i * BlockSize + j]; aes.EncryptEcb(x.AsSpan(), x.AsSpan(), PaddingMode.None); } byte[] last = new byte[BlockSize]; int lastStart = (numBlocks - 1) * BlockSize; int remaining = n - lastStart; if (lastComplete) { Buffer.BlockCopy(data, lastStart, last, 0, BlockSize); XorInPlace(last, K1, BlockSize); } else { if (remaining > 0) Buffer.BlockCopy(data, lastStart, last, 0, remaining); last[remaining] = 0x80; XorInPlace(last, K2, BlockSize); } XorInPlace(x, last, BlockSize); byte[] result = new byte[BlockSize]; aes.EncryptEcb(x.AsSpan(), result.AsSpan(), PaddingMode.None); return result; } private static byte[] EaxOmac(byte[] key, byte tag, ReadOnlySpan data) { byte[] input = new byte[BlockSize + data.Length]; input[BlockSize - 1] = tag; data.CopyTo(input.AsSpan(BlockSize)); return Cmac(key, input); } public static (byte[] ciphertext, byte[] mac) EaxEncrypt(byte[] key, byte[] nonce, byte[] header, byte[] plaintext) { byte[] N = EaxOmac(key, 0, nonce); byte[] H = EaxOmac(key, 1, header); byte[] ciphertext = AesCtr(key, N, plaintext); byte[] C = EaxOmac(key, 2, ciphertext); byte[] mac = new byte[MacLen]; for (int i = 0; i < MacLen; i++) mac[i] = (byte)(N[i] ^ H[i] ^ C[i]); return (ciphertext, mac); } public static byte[] EaxDecrypt(byte[] key, byte[] nonce, ReadOnlySpan header, ReadOnlySpan ciphertext, ReadOnlySpan mac) { byte[] N = EaxOmac(key, 0, nonce); byte[] H = EaxOmac(key, 1, header); byte[] C = EaxOmac(key, 2, ciphertext); byte[] tag = new byte[MacLen]; for (int i = 0; i < MacLen; i++) tag[i] = (byte)(N[i] ^ H[i] ^ C[i]); if (!CryptographicOperations.FixedTimeEquals(tag, mac)) return null; return AesCtr(key, N, ciphertext); } private static byte[] AesCtr(byte[] key, byte[] counter, ReadOnlySpan data) { if (data.Length == 0) return Array.Empty(); using var aes = Aes.Create(); aes.Key = key; byte[] output = new byte[data.Length]; byte[] ctr = (byte[])counter.Clone(); byte[] ks = new byte[BlockSize]; for (int offset = 0; offset < data.Length; offset += BlockSize) { aes.EncryptEcb(ctr.AsSpan(), ks.AsSpan(), PaddingMode.None); int len = Math.Min(BlockSize, data.Length - offset); for (int i = 0; i < len; i++) output[offset + i] = (byte)(data[offset + i] ^ ks[i]); IncrementBigEndian(ctr); } return output; } private static void IncrementBigEndian(byte[] ctr) { for (int i = 15; i >= 0; i--) if (++ctr[i] != 0) break; } /// Derives the per-packet AES-EAX key and nonce from ivStruct. public static (byte[] key, byte[] nonce) DeriveKeyNonce( bool fromServer, ushort packetId, uint generationId, byte packetType, byte[] ivStruct) { // ivStruct is 20 bytes for TS3, 64 bytes for TS6 int tmpLen = ivStruct.Length == 20 ? 26 : 70; byte[] tmp = new byte[tmpLen]; tmp[0] = fromServer ? (byte)0x30 : (byte)0x31; tmp[1] = packetType; tmp[2] = (byte)(generationId >> 24); tmp[3] = (byte)(generationId >> 16); tmp[4] = (byte)(generationId >> 8); tmp[5] = (byte)generationId; Buffer.BlockCopy(ivStruct, 0, tmp, 6, ivStruct.Length); byte[] kn = SHA256.HashData(tmp); byte[] key = new byte[16]; byte[] nonce = new byte[16]; Buffer.BlockCopy(kn, 0, key, 0, 16); Buffer.BlockCopy(kn, 16, nonce, 0, 16); key[0] ^= (byte)(packetId >> 8); key[1] ^= (byte)packetId; return (key, nonce); } public static string HashPassword(string password) { if (string.IsNullOrEmpty(password)) return ""; return Convert.ToBase64String(SHA1.HashData(Encoding.UTF8.GetBytes(password))); } public static void XorInto(byte[] dest, byte[] src, int len) { for (int i = 0; i < len; i++) dest[i] ^= src[i]; } private static void XorInPlace(byte[] a, byte[] b, int len) { for (int i = 0; i < len; i++) a[i] ^= b[i]; } private static byte[] DoubleBlock(byte[] buf) { byte[] out_ = new byte[BlockSize]; int carry = 0; for (int i = BlockSize - 1; i >= 0; i--) { int tmp = (buf[i] << 1) | carry; out_[i] = (byte)tmp; carry = (buf[i] >> 7) & 1; } if (carry != 0) out_[BlockSize - 1] ^= Rb; return out_; } } }