using System.Buffers.Binary; using System.Numerics; namespace Ananta.SDK.Network; /// /// Exact stream cipher used by the 4229938 UXNetwork client. /// It is ChaCha8-shaped, but the four sigma constants are client-specific. /// internal sealed class UxChaCha8 { // GameAssembly.dll 4229938 / UXChaCha8Impl::.ctor // NOTE: these deliberately differ from standard "expand 32-byte k". private const uint Sigma0 = 0x60707865; private const uint Sigma1 = 0x3321646e; private const uint Sigma2 = 0x79622d32; private const uint Sigma3 = 0x6b206e74; private readonly uint[] _state = new uint[16]; private readonly byte[] _keystream = new byte[64]; private int _keystreamPos = 64; public UxChaCha8(ReadOnlySpan key, ReadOnlySpan nonce) { if (key.Length != 32) throw new ArgumentException("UXChaCha8 key must be 32 bytes.", nameof(key)); if (nonce.Length != 12) throw new ArgumentException("UXChaCha8 nonce must be 12 bytes.", nameof(nonce)); _state[0] = Sigma0; _state[1] = Sigma1; _state[2] = Sigma2; _state[3] = Sigma3; for (var i = 0; i < 8; i++) _state[4 + i] = BinaryPrimitives.ReadUInt32LittleEndian(key.Slice(i * 4, 4)); // Client constructor sets word 12 to zero and uses a 96-bit nonce. _state[12] = 0; _state[13] = BinaryPrimitives.ReadUInt32LittleEndian(nonce.Slice(0, 4)); _state[14] = BinaryPrimitives.ReadUInt32LittleEndian(nonce.Slice(4, 4)); _state[15] = BinaryPrimitives.ReadUInt32LittleEndian(nonce.Slice(8, 4)); } public void XorInPlace(Span data) { var offset = 0; while (offset < data.Length) { if (_keystreamPos == 64) { GenerateBlock(); _keystreamPos = 0; } var take = Math.Min(64 - _keystreamPos, data.Length - offset); for (var i = 0; i < take; i++) data[offset + i] ^= _keystream[_keystreamPos + i]; offset += take; _keystreamPos += take; } } private void GenerateBlock() { Span x = stackalloc uint[16]; _state.AsSpan().CopyTo(x); // UXChaCha8 is 8 rounds = four column+diagonal double-rounds. for (var i = 0; i < 4; i++) { QuarterRound(ref x[0], ref x[4], ref x[8], ref x[12]); QuarterRound(ref x[1], ref x[5], ref x[9], ref x[13]); QuarterRound(ref x[2], ref x[6], ref x[10], ref x[14]); QuarterRound(ref x[3], ref x[7], ref x[11], ref x[15]); QuarterRound(ref x[0], ref x[5], ref x[10], ref x[15]); QuarterRound(ref x[1], ref x[6], ref x[11], ref x[12]); QuarterRound(ref x[2], ref x[7], ref x[8], ref x[13]); QuarterRound(ref x[3], ref x[4], ref x[9], ref x[14]); } for (var i = 0; i < 16; i++) BinaryPrimitives.WriteUInt32LittleEndian(_keystream.AsSpan(i * 4, 4), unchecked(x[i] + _state[i])); // Client increments only the 32-bit block counter (state[12]). _state[12] = unchecked(_state[12] + 1); } private static void QuarterRound(ref uint a, ref uint b, ref uint c, ref uint d) { a = unchecked(a + b); d ^= a; d = BitOperations.RotateLeft(d, 16); c = unchecked(c + d); b ^= c; b = BitOperations.RotateLeft(b, 12); a = unchecked(a + b); d ^= a; d = BitOperations.RotateLeft(d, 8); c = unchecked(c + d); b ^= c; b = BitOperations.RotateLeft(b, 7); } }