using System; using System.Collections.Generic; using System.Security.Cryptography; namespace axXez.TS3.Protocol { internal static class VoiceLicense { // TeamSpeak license root key (Ed25519 compressed point) private static readonly byte[] LicenseRootKey = { 0xcd, 0x0d, 0xe2, 0xae, 0xd4, 0x63, 0x45, 0x50, 0x9a, 0x7e, 0x3c, 0xfd, 0x8f, 0x68, 0xb3, 0xdc, 0x75, 0x55, 0xb2, 0x9d, 0xcc, 0xec, 0x73, 0xcd, 0x18, 0x75, 0x0f, 0x99, 0x38, 0x12, 0x40, 0x8a, }; internal struct LicenseBlock { public byte[] Key; // 32-byte Ed25519 point public byte[] Hash; // first 32 bytes of SHA-512 of block content public int Type; } public static LicenseBlock[] ParseLicense(byte[] data) { if (data.Length < 1) throw new FormatException("License data too short."); if (data[0] != 1) throw new FormatException($"Unsupported license version {data[0]}."); var blocks = new List(); int pos = 1; while (pos < data.Length) { if (data.Length - pos < 42) throw new FormatException("License block too short."); if (data[pos] != 0) throw new FormatException($"Unexpected key kind {data[pos]} in license block."); byte[] key = data.AsSpan(pos + 1, 32).ToArray(); int blockType = data[pos + 33]; int extraLen; switch (blockType) { case 0: // Intermediate extraLen = FindNull(data, pos + 46) + 5; break; case 2: // Server extraLen = FindNull(data, pos + 47) + 6; break; case 8: // TS5/TS6 Server { int p = pos + 44; int propCount = data[pos + 43]; for (int i = 0; i < propCount; i++) p += 1 + data[p]; extraLen = p - (pos + 42); break; } case 32: // Ephemeral extraLen = 0; break; default: throw new FormatException($"Invalid license block type {blockType}."); } int blockLen = 42 + extraLen; byte[] sha512 = SHA512.HashData(data.AsSpan(pos + 1, blockLen - 1)); byte[] hash = sha512.AsSpan(0, 32).ToArray(); blocks.Add(new LicenseBlock { Key = key, Hash = hash, Type = blockType }); pos += blockLen; } return blocks.ToArray(); } private static int FindNull(byte[] data, int start) { for (int i = start; i < data.Length; i++) if (data[i] == 0) return i - start; throw new FormatException("Non-null-terminated string in license block."); } /// /// Derives the final server Ed25519 public key from the license block chain. /// Each block contributes via scalar·blockKey + parentKey on Edwards25519. /// public static byte[] DeriveLicenseKey(LicenseBlock[] blocks) { byte[] parentKey = (byte[])LicenseRootKey.Clone(); foreach (var block in blocks) parentKey = DeriveBlockKey(block.Key, block.Hash, parentKey); return parentKey; } private static byte[] DeriveBlockKey(byte[] blockKey, byte[] hash, byte[] parentKey) { // Clamp scalar from block hash (same clamping as X25519) byte[] scalar = (byte[])hash.Clone(); scalar[0] &= 0xf8; scalar[31] &= 0x3f; scalar[31] |= 0x40; byte[] mulResult = Edwards25519.ScalarMult(scalar, blockKey); return Edwards25519.PointAdd(mulResult, parentKey); } /// Generates a clamped random scalar and its Edwards25519 public key point. public static (byte[] publicKey, byte[] privateKey) GenerateTemporaryKey() { byte[] privateKey = RandomNumberGenerator.GetBytes(32); privateKey[0] &= 0xf8; privateKey[31] &= 0x7f; privateKey[31] |= 0x40; byte[] publicKey = Edwards25519.ScalarMultBase(privateKey); return (publicKey, privateKey); } /// /// Computes SHA-512(privKeyCopy * serverDerivedKey) on Edwards25519. /// Returns 64 bytes used as ivStruct for the new protocol. /// public static byte[] GetSharedSecret2(byte[] serverDerivedKey, byte[] tempPrivateKey) { byte[] privKeyCopy = (byte[])tempPrivateKey.Clone(); privKeyCopy[31] &= 0x7f; // clear the top bit before scalar mult byte[] mulResult = Edwards25519.ScalarMult(privKeyCopy, serverDerivedKey); return SHA512.HashData(mulResult); } } }