using System; using System.Numerics; using System.Security.Cryptography; namespace Intelix.Helper.Encrypted; public static class ChaCha20Poly1305 { public static byte[] Decrypt(byte[] key32, byte[] iv12, byte[] ciphertext, byte[] tag, byte[] aad = null) { if (key32 == null) { throw new ArgumentNullException("key32"); } if (key32.Length != 32) { throw new ArgumentException("Key must be 32 bytes", "key32"); } if (iv12 == null) { throw new ArgumentNullException("iv12"); } if (iv12.Length != 12) { throw new ArgumentException("IV must be 12 bytes", "iv12"); } if (ciphertext == null) { throw new ArgumentNullException("ciphertext"); } if (tag == null) { throw new ArgumentNullException("tag"); } if (tag.Length != 16) { throw new ArgumentException("Tag must be 16 bytes", "tag"); } if (aad == null) { aad = Array.Empty(); } byte[] array = ChaCha20Block(key32, 0u, iv12); byte[] array2 = new byte[32]; Buffer.BlockCopy(array, 0, array2, 0, 32); byte[] msg = BuildPoly1305Message(aad, ciphertext); if (!FixedTimeEquals(Poly1305TagWithBigInteger(array2, msg), tag)) { Array.Clear(array, 0, array.Length); Array.Clear(array2, 0, array2.Length); throw new CryptographicException("ChaCha20-Poly1305 authentication failed (tag mismatch)."); } byte[] array3 = new byte[ciphertext.Length]; ChaCha20Xor(key32, 1u, iv12, ciphertext, array3); Array.Clear(array, 0, array.Length); Array.Clear(array2, 0, array2.Length); return array3; } private static byte[] ChaCha20Block(byte[] key32, uint counter, byte[] nonce12) { uint[] array = new uint[16] { 1634760805u, 857760878u, 2036477234u, 1797285236u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u }; for (int i = 0; i < 8; i++) { array[4 + i] = ToUInt32Little(key32, i * 4); } array[12] = counter; array[13] = ToUInt32Little(nonce12, 0); array[14] = ToUInt32Little(nonce12, 4); array[15] = ToUInt32Little(nonce12, 8); uint[] array2 = new uint[16]; Array.Copy(array, array2, 16); for (int j = 0; j < 10; j++) { QuarterRound(ref array2[0], ref array2[4], ref array2[8], ref array2[12]); QuarterRound(ref array2[1], ref array2[5], ref array2[9], ref array2[13]); QuarterRound(ref array2[2], ref array2[6], ref array2[10], ref array2[14]); QuarterRound(ref array2[3], ref array2[7], ref array2[11], ref array2[15]); QuarterRound(ref array2[0], ref array2[5], ref array2[10], ref array2[15]); QuarterRound(ref array2[1], ref array2[6], ref array2[11], ref array2[12]); QuarterRound(ref array2[2], ref array2[7], ref array2[8], ref array2[13]); QuarterRound(ref array2[3], ref array2[4], ref array2[9], ref array2[14]); } byte[] array3 = new byte[64]; for (int k = 0; k < 16; k++) { LittleEndian(array2[k] + array[k], array3, k * 4); } return array3; } private static void QuarterRound(ref uint a, ref uint b, ref uint c, ref uint d) { a += b; d ^= a; d = Rol(d, 16); c += d; b ^= c; b = Rol(b, 12); a += b; d ^= a; d = Rol(d, 8); c += d; b ^= c; b = Rol(b, 7); } private static uint Rol(uint x, int n) { return (x << n) | (x >> 32 - n); } private static uint ToUInt32Little(byte[] bs, int off) { return (uint)(bs[off] | (bs[off + 1] << 8) | (bs[off + 2] << 16) | (bs[off + 3] << 24)); } private static void LittleEndian(uint v, byte[] outbuf, int off) { outbuf[off] = (byte)(v & 0xFF); outbuf[off + 1] = (byte)((v >> 8) & 0xFF); outbuf[off + 2] = (byte)((v >> 16) & 0xFF); outbuf[off + 3] = (byte)((v >> 24) & 0xFF); } private static void ChaCha20Xor(byte[] key, uint counter, byte[] nonce, byte[] input, byte[] output) { if (input == null || input.Length == 0) { return; } int i = 0; uint num = counter; int num2; for (; i < input.Length; i += num2) { byte[] array = ChaCha20Block(key, num, nonce); num++; num2 = Math.Min(64, input.Length - i); for (int j = 0; j < num2; j++) { output[i + j] = (byte)(input[i + j] ^ array[j]); } } } private static byte[] BuildPoly1305Message(byte[] aad, byte[] ciphertext) { int num = ((aad != null) ? aad.Length : 0); int num2 = ((ciphertext != null) ? ciphertext.Length : 0); int num3 = (16 - num % 16) % 16; int num4 = (16 - num2 % 16) % 16; byte[] array = new byte[num + num3 + num2 + num4 + 8 + 8]; int num5 = 0; if (num > 0) { Buffer.BlockCopy(aad, 0, array, num5, num); num5 += num; } if (num3 > 0) { num5 += num3; } if (num2 > 0) { Buffer.BlockCopy(ciphertext, 0, array, num5, num2); num5 += num2; } if (num4 > 0) { num5 += num4; } byte[] bytes = BitConverter.GetBytes((ulong)num); byte[] bytes2 = BitConverter.GetBytes((ulong)num2); Buffer.BlockCopy(bytes, 0, array, num5, 8); num5 += 8; Buffer.BlockCopy(bytes2, 0, array, num5, 8); num5 += 8; return array; } private static byte[] Poly1305TagWithBigInteger(byte[] oneTimeKey32, byte[] msg) { byte[] array = new byte[16]; Buffer.BlockCopy(oneTimeKey32, 0, array, 0, 16); array[3] &= 15; array[7] &= 15; array[11] &= 15; array[15] &= 15; array[4] &= 252; array[8] &= 252; array[12] &= 252; byte[] array2 = new byte[16]; Buffer.BlockCopy(oneTimeKey32, 16, array2, 0, 16); BigInteger bigInteger = new BigInteger(AppendZero(array)); BigInteger bigInteger2 = new BigInteger(AppendZero(array2)); BigInteger bigInteger3 = (BigInteger.One << 130) - 5; BigInteger bigInteger4 = BigInteger.Zero; for (int i = 0; i < msg.Length; i += 16) { int num = Math.Min(16, msg.Length - i); byte[] array3 = new byte[num]; Buffer.BlockCopy(msg, i, array3, 0, num); BigInteger bigInteger5 = new BigInteger(AppendZero(array3)); BigInteger bigInteger6 = BigInteger.One << 8 * num; bigInteger5 += bigInteger6; bigInteger4 += bigInteger5; bigInteger4 = bigInteger4 * bigInteger % bigInteger3; } byte[] array4 = (bigInteger4 + bigInteger2).ToByteArray(); byte[] array5 = new byte[16]; for (int j = 0; j < 16 && j < array4.Length; j++) { array5[j] = array4[j]; } return array5; } private static byte[] AppendZero(byte[] b) { byte[] array = new byte[b.Length + 1]; Buffer.BlockCopy(b, 0, array, 0, b.Length); array[array.Length - 1] = 0; return array; } private static bool FixedTimeEquals(byte[] a, byte[] b) { if (a == null || b == null || a.Length != b.Length) { return false; } int num = 0; for (int i = 0; i < a.Length; i++) { num |= a[i] ^ b[i]; } return num == 0; } }