diff --git a/Transports/com.mlapi.contrib.transport.chacha20/CHANGELOG.md b/Transports/com.mlapi.contrib.transport.chacha20/CHANGELOG.md
new file mode 100644
index 00000000..32bde89d
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+
+## 1.0.0
+First version of the ChaCha20 transport as a Unity package.
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/CHANGELOG.md.meta b/Transports/com.mlapi.contrib.transport.chacha20/CHANGELOG.md.meta
new file mode 100644
index 00000000..cedb450f
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/CHANGELOG.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0a024bb1894e44e4b84f176b0b386f24
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/LICENSE.md b/Transports/com.mlapi.contrib.transport.chacha20/LICENSE.md
new file mode 100644
index 00000000..ea4b0085
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/LICENSE.md
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2021 Unity Technologies
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/LICENSE.md.meta b/Transports/com.mlapi.contrib.transport.chacha20/LICENSE.md.meta
new file mode 100644
index 00000000..0c9c2353
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/LICENSE.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 56fa6d8e6a036b74eaef27818e3698c4
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/README.md b/Transports/com.mlapi.contrib.transport.chacha20/README.md
new file mode 100644
index 00000000..180c7365
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/README.md
@@ -0,0 +1 @@
+ChaCha20 transport for MLAPI
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/README.md.meta b/Transports/com.mlapi.contrib.transport.chacha20/README.md.meta
new file mode 100644
index 00000000..99224e0f
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8ffe30828b308c245b69db7e4a4787d8
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime.meta
new file mode 100644
index 00000000..2ab461dd
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d8bfbe123de7f4140b6b2071b1a7cbfa
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20.meta
new file mode 100644
index 00000000..19313ca0
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ccce8b0bffa74671925ae87d227fae1e
+timeCreated: 1617966330
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/ChaCha20Cipher.cs b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/ChaCha20Cipher.cs
new file mode 100644
index 00000000..c1b6a963
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/ChaCha20Cipher.cs
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2015, 2018 Scott Bennett
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+// Modified by Albin Corén, https://github.com/twotenpvp (@Unity Technologies)
+
+using System;
+using System.Text;
+
+namespace MLAPI.Transport.ChaCha20.ChaCha20
+{
+ public sealed class ChaCha20Cipher : IDisposable
+ {
+ ///
+ /// The ChaCha20 state (aka "context")
+ ///
+ private uint[] m_State;
+
+ ///
+ /// Determines if the objects in this class have been disposed of. Set to
+ /// true by the Dispose() method.
+ ///
+ private bool m_IsDisposed;
+
+ private readonly byte[] m_Key;
+ private readonly byte[] m_Nonce;
+ private uint m_Counter;
+
+ ///
+ /// Set up a new ChaCha20 state. The lengths of the given parameters are
+ /// checked before encryption happens.
+ ///
+ ///
+ /// See ChaCha20 Spec Section 2.4
+ /// for a detailed description of the inputs.
+ ///
+ ///
+ /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit
+ /// little-endian integers
+ ///
+ ///
+ /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit
+ /// little-endian integers
+ ///
+ ///
+ /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
+ ///
+ public ChaCha20Cipher(byte[] key, byte[] nonce, uint counter)
+ {
+ m_State = new uint[16];
+ m_IsDisposed = false;
+
+ m_Key = key;
+ m_Nonce = nonce;
+
+ KeySetup(key);
+ IvSetup(nonce, counter);
+ }
+
+ ///
+ /// The ChaCha20 state (aka "context"). Read-Only.
+ ///
+ public uint[] State => m_State;
+
+ public uint Counter
+ {
+ get => m_Counter;
+ set => IvSetup(m_Nonce, value);
+ }
+
+ public void SetCounter(uint counter)
+ {
+ IvSetup(m_Nonce, counter);
+ }
+
+ ///
+ /// Set up the ChaCha state with the given key. A 32-byte key is required
+ /// and enforced.
+ ///
+ ///
+ /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit
+ /// little-endian integers
+ ///
+ private void KeySetup(byte[] key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException("Key is null");
+ }
+
+ if (key.Length != 32)
+ {
+ throw new ArgumentException(
+ $"Key length must be 32. Actual: {key.Length}"
+ );
+ }
+
+ // These are the same constants defined in the reference implementation.
+ // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c
+ byte[] sigma = Encoding.ASCII.GetBytes("expand 32-byte k");
+ byte[] tau = Encoding.ASCII.GetBytes("expand 16-byte k");
+
+ m_State[4] = Util.U8To32Little(key, 0);
+ m_State[5] = Util.U8To32Little(key, 4);
+ m_State[6] = Util.U8To32Little(key, 8);
+ m_State[7] = Util.U8To32Little(key, 12);
+
+ byte[] constants = (key.Length == 32) ? sigma : tau;
+ int keyIndex = key.Length - 16;
+
+ m_State[8] = Util.U8To32Little(key, keyIndex + 0);
+ m_State[9] = Util.U8To32Little(key, keyIndex + 4);
+ m_State[10] = Util.U8To32Little(key, keyIndex + 8);
+ m_State[11] = Util.U8To32Little(key, keyIndex + 12);
+
+ m_State[0] = Util.U8To32Little(constants, 0);
+ m_State[1] = Util.U8To32Little(constants, 4);
+ m_State[2] = Util.U8To32Little(constants, 8);
+ m_State[3] = Util.U8To32Little(constants, 12);
+ }
+
+ ///
+ /// Set up the ChaCha state with the given nonce (aka Initialization Vector
+ /// or IV) and block counter. A 12-byte nonce and a 4-byte counter are
+ /// required.
+ ///
+ ///
+ /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit
+ /// little-endian integers
+ ///
+ ///
+ /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
+ ///
+ private void IvSetup(byte[] nonce, uint counter)
+ {
+ if (nonce == null)
+ {
+ // There has already been some state set up. Clear it before exiting.
+ Dispose();
+ throw new ArgumentNullException("Nonce is null");
+ }
+
+ if (nonce.Length != 12)
+ {
+ // There has already been some state set up. Clear it before exiting.
+ Dispose();
+ throw new ArgumentException(
+ $"Nonce length must be 12. Actual: {nonce.Length}"
+ );
+ }
+
+ m_State[12] = counter;
+ m_State[13] = Util.U8To32Little(nonce, 0);
+ m_State[14] = Util.U8To32Little(nonce, 4);
+ m_State[15] = Util.U8To32Little(nonce, 8);
+
+ m_Counter = counter;
+ }
+
+ ///
+ /// Encrypt an arbitrary-length plaintext message (input), writing the
+ /// resulting ciphertext to the output buffer. The number of bytes to read
+ /// from the input buffer is determined by numBytes.
+ ///
+ ///
+ ///
+ ///
+ public void ProcessBytes(byte[] input, int inputOffset, byte[] output, int outputOffset, int count)
+ {
+ if (m_IsDisposed)
+ {
+ throw new ObjectDisposedException("state", "The ChaCha state has been disposed");
+ }
+
+ if (count < 0 || count > input.Length - inputOffset)
+ {
+ throw new ArgumentOutOfRangeException("count", "The number of bytes to read must be between [0..input.Length]");
+ }
+
+ uint[] x = new uint[16]; // Working buffer
+ byte[] tmp = new byte[64]; // Temporary buffer
+ int outputPosition = 0;
+ int inputPosition = 0;
+
+ while (count > 0)
+ {
+ for (int i = 16; i-- > 0;)
+ {
+ x[i] = m_State[i];
+ }
+
+ for (int i = 20; i > 0; i -= 2)
+ {
+ QuarterRound(x, 0, 4, 8, 12);
+ QuarterRound(x, 1, 5, 9, 13);
+ QuarterRound(x, 2, 6, 10, 14);
+ QuarterRound(x, 3, 7, 11, 15);
+
+ QuarterRound(x, 0, 5, 10, 15);
+ QuarterRound(x, 1, 6, 11, 12);
+ QuarterRound(x, 2, 7, 8, 13);
+ QuarterRound(x, 3, 4, 9, 14);
+ }
+
+ for (int i = 16; i-- > 0;)
+ {
+ Util.ToBytes(tmp, Util.Add(x[i], m_State[i]), 4 * i);
+ }
+
+ m_State[12] = Util.AddOne(m_State[12]);
+ if (m_State[12] <= 0)
+ {
+ /* Stopping at 2^70 bytes per nonce is the user's responsibility */
+ m_State[13] = Util.AddOne(m_State[13]);
+ }
+
+ m_Counter++;
+
+ if (count <= 64)
+ {
+ for (int i = count; i-- > 0;)
+ {
+ output[i + outputPosition + outputOffset] = (byte) (input[i + inputPosition + inputOffset] ^ tmp[i]);
+ }
+
+ return;
+ }
+
+ for (int i = 64; i-- > 0;)
+ {
+ output[i + outputPosition + outputOffset] = (byte) (input[i + inputPosition + inputOffset] ^ tmp[i]);
+ }
+
+ count -= 64;
+ outputPosition += 64;
+ inputPosition += 64;
+ }
+ }
+
+ ///
+ /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned
+ /// integers within the given buffer at indices a, b, c, and d.
+ ///
+ ///
+ /// The ChaCha state does not have four integer numbers: it has 16. So
+ /// the quarter-round operation works on only four of them -- hence the
+ /// name. Each quarter round operates on four predetermined numbers in
+ /// the ChaCha state.
+ /// See ChaCha20 Spec Sections 2.1 - 2.2.
+ ///
+ /// A ChaCha state (vector). Must contain 16 elements.
+ /// Index of the first number
+ /// Index of the second number
+ /// Index of the third number
+ /// Index of the fourth number
+ public static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d)
+ {
+ if (x == null)
+ {
+ throw new ArgumentNullException("Input buffer is null");
+ }
+
+ if (x.Length != 16)
+ {
+ throw new ArgumentException();
+ }
+
+ x[a] = Util.Add(x[a], x[b]);
+ x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16);
+ x[c] = Util.Add(x[c], x[d]);
+ x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12);
+ x[a] = Util.Add(x[a], x[b]);
+ x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8);
+ x[c] = Util.Add(x[c], x[d]);
+ x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7);
+ }
+
+ #region Destructor and Disposer
+
+ ///
+ /// Clear and dispose of the internal state. The finalizer is only called
+ /// if Dispose() was never called on this cipher.
+ ///
+ ~ChaCha20Cipher()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Clear and dispose of the internal state. Also request the GC not to
+ /// call the finalizer, because all cleanup has been taken care of.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ /*
+ * The Garbage Collector does not need to invoke the finalizer because
+ * Dispose(bool) has already done all the cleanup needed.
+ */
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// This method should only be invoked from Dispose() or the finalizer.
+ /// This handles the actual cleanup of the resources.
+ ///
+ ///
+ /// Should be true if called by Dispose(); false if called by the finalizer
+ ///
+ private void Dispose(bool disposing)
+ {
+ if (!m_IsDisposed)
+ {
+ if (disposing)
+ {
+ /* Cleanup managed objects by calling their Dispose() methods */
+ }
+
+ /* Cleanup any unmanaged objects here */
+ if (m_State != null)
+ {
+ Array.Clear(m_State, 0, m_State.Length);
+ }
+
+ m_State = null;
+ }
+
+ m_IsDisposed = true;
+ }
+
+ #endregion
+ }
+}
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/ChaCha20Cipher.cs.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/ChaCha20Cipher.cs.meta
new file mode 100644
index 00000000..43e4ff72
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/ChaCha20Cipher.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 314b25bc369a4358bcc275b5b4eda2ba
+timeCreated: 1617966199
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/Util.cs b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/Util.cs
new file mode 100644
index 00000000..c98a85da
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/Util.cs
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2015, 2018 Scott Bennett
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+// Modified by Albin Corén, https://github.com/twotenpvp (@Unity Technologies)
+
+using System;
+
+namespace MLAPI.Transport.ChaCha20.ChaCha20
+{
+ public class Util
+ {
+ ///
+ /// n-bit left rotation operation (towards the high bits) for 32-bit
+ /// integers.
+ ///
+ ///
+ ///
+ /// The result of (v LEFTSHIFT c)
+ public static uint Rotate(uint v, int c)
+ {
+ unchecked
+ {
+ return (v << c) | (v >> (32 - c));
+ }
+ }
+
+ ///
+ /// Unchecked integer exclusive or (XOR) operation.
+ ///
+ ///
+ ///
+ /// The result of (v XOR w)
+ public static uint XOr(uint v, uint w)
+ {
+ return unchecked(v ^ w);
+ }
+
+ ///
+ /// Unchecked integer addition. The ChaCha spec defines certain operations
+ /// to use 32-bit unsigned integer addition modulo 2^32.
+ ///
+ ///
+ ///
+ /// See ChaCha20 Spec Section 2.1.
+ ///
+ ///
+ ///
+ ///
+ /// The result of (v + w) modulo 2^32
+ public static uint Add(uint v, uint w)
+ {
+ return unchecked(v + w);
+ }
+
+ ///
+ /// Add 1 to the input parameter using unchecked integer addition. The
+ /// ChaCha spec defines certain operations to use 32-bit unsigned integer
+ /// addition modulo 2^32.
+ ///
+ ///
+ /// See ChaCha20 Spec Section 2.1.
+ ///
+ ///
+ /// The result of (v + 1) modulo 2^32
+ public static uint AddOne(uint v)
+ {
+ return unchecked(v + 1);
+ }
+
+ ///
+ /// Convert four bytes of the input buffer into an unsigned
+ /// 32-bit integer, beginning at the inputOffset.
+ ///
+ ///
+ ///
+ /// An unsigned 32-bit integer
+ public static uint U8To32Little(byte[] p, int inputOffset)
+ {
+ unchecked
+ {
+ return ((uint) p[inputOffset]
+ | ((uint) p[inputOffset + 1] << 8)
+ | ((uint) p[inputOffset + 2] << 16)
+ | ((uint) p[inputOffset + 3] << 24));
+ }
+ }
+
+ ///
+ /// Serialize the input integer into the output buffer. The input integer
+ /// will be split into 4 bytes and put into four sequential places in the
+ /// output buffer, starting at the outputOffset.
+ ///
+ ///
+ ///
+ ///
+ public static void ToBytes(byte[] output, uint input, int outputOffset)
+ {
+ if (outputOffset < 0)
+ {
+ throw new ArgumentOutOfRangeException("outputOffset", "The buffer offset cannot be negative");
+ }
+
+ unchecked
+ {
+ output[outputOffset] = (byte) input;
+ output[outputOffset + 1] = (byte) (input >> 8);
+ output[outputOffset + 2] = (byte) (input >> 16);
+ output[outputOffset + 3] = (byte) (input >> 24);
+ }
+ }
+ }
+}
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/Util.cs.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/Util.cs.meta
new file mode 100644
index 00000000..9d2a2290
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/ChaCha20/Util.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d6bf41c930544459a55c60278cb25a87
+timeCreated: 1617966350
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/CryptographyTransportAdapter.cs b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/CryptographyTransportAdapter.cs
new file mode 100644
index 00000000..23826e4d
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/CryptographyTransportAdapter.cs
@@ -0,0 +1,690 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using MLAPI.Cryptography.KeyExchanges;
+using MLAPI.Cryptography.Utils;
+using MLAPI.Transport.ChaCha20.ChaCha20;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.Assertions;
+
+namespace MLAPI.Transport.ChaCha20
+{
+ public class CryptographyTransportAdapter : NetworkTransport
+ {
+ public override ulong ServerClientId => Transport.ServerClientId;
+
+ public NetworkTransport Transport;
+
+ public bool SignKeyExchange;
+
+ [TextArea]
+ public string ServerBase64PFX;
+
+ [Tooltip("This adds significant CPU overhead and a 32 byte overhead on each message")]
+ public bool SignEveryMessage = false;
+
+ public Func ValidateCertificate;
+
+ private X509Certificate2 m_ServerCertificate;
+
+ private byte[] m_ServerCertificateBytes
+ {
+ get
+ {
+ if (m_ServerCertificatesByteBacking == null)
+ {
+ m_ServerCertificatesByteBacking = m_ServerCertificate.Export(X509ContentType.Cert);
+ }
+
+ return m_ServerCertificatesByteBacking;
+ }
+ }
+
+ private byte[] m_ServerCertificatesByteBacking;
+
+ // State
+ private bool m_IsServer;
+
+ // Used by client
+ private ECDiffieHellmanRSA m_ServerSignedKeyExchange;
+ private ECDiffieHellman m_ServerKeyExchange;
+
+ // Used by server
+ private readonly Dictionary m_ClientSignedKeyExchanges = new Dictionary();
+ private readonly Dictionary m_ClientKeyExchanges = new Dictionary();
+
+ // Public keys for external use
+ public byte[] ServerKey { get; private set; }
+ public readonly Dictionary ClientKeys = new Dictionary();
+
+ // Ciphers
+ private readonly Dictionary m_ClientSendCiphers = new Dictionary();
+ private readonly Dictionary m_ClientReceiveCiphers = new Dictionary();
+
+ private ChaCha20Cipher m_ServerSendCipher;
+ private ChaCha20Cipher m_ServerReceiveCipher;
+
+ // HMACs
+ private readonly Dictionary m_ClientAuthenticators = new Dictionary();
+ private HMACSHA256 m_ServerAuthenticator;
+
+ // States
+ private readonly Dictionary m_ClientStates = new Dictionary();
+
+ private enum ClientState : byte
+ {
+ WaitingForHailResponse,
+ Connected
+ }
+
+ // Max message size
+ private readonly byte[] m_CryptoBuffer = new byte[1024 * 8];
+ private readonly byte[] m_WriteBuffer = new byte[1024 * 8];
+
+ private readonly byte[] m_AuthenticationBuffer = new byte[32];
+
+ private enum MessageType : byte
+ {
+ Hail, // Server->Client
+ HailResponse, // Client->Server
+ Ready, // Server->Client
+ Internal // MLAPI Message
+ }
+
+ public override void Send(ulong clientId, ArraySegment data, NetworkDelivery networkDelivery)
+ {
+ // Write message type
+ m_WriteBuffer[0] = (byte) MessageType.Internal;
+
+ // Get the ChaCha20 cipher
+ ChaCha20Cipher cipher = clientId == ServerClientId ? m_ServerSendCipher : m_ClientSendCiphers[clientId];
+
+ // Write least significant bits of counter
+ m_WriteBuffer[1] = (byte) cipher.Counter;
+
+ // Encrypt with ChaCha
+ cipher.ProcessBytes(data.Array, data.Offset, m_WriteBuffer, 2, data.Count);
+
+ if (SignEveryMessage)
+ {
+ // Get authenticator
+ HMACSHA256 authenticator = clientId == ServerClientId ? m_ServerAuthenticator : m_ClientAuthenticators[clientId];
+
+ // Calculate signature
+ byte[] signature = authenticator.ComputeHash(m_WriteBuffer, 0, 2 + data.Count);
+
+ // Copy signature
+ Buffer.BlockCopy(signature, 0, m_WriteBuffer, 2 + data.Count, signature.Length);
+ }
+
+ // Send the encrypted format
+ Transport.Send(clientId, new ArraySegment(m_WriteBuffer, 0, 2 + data.Count + (SignEveryMessage ? 32 : 0)), networkDelivery);
+ }
+
+ public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime)
+ {
+ NetworkEvent @event = Transport.PollEvent(out ulong internalClientId, out ArraySegment internalPayload, out float internalReceiveTime);
+
+ if (@event == NetworkEvent.Connect && m_IsServer && !m_ClientStates.ContainsKey(internalClientId))
+ {
+ // Write message type
+ m_WriteBuffer[0] = (byte)((byte)MessageType.Hail | ((SignKeyExchange ? (byte)1 : (byte)0) << 7) | ((SignEveryMessage ? (byte) 1 : (byte) 0) << 6));
+
+ int length;
+
+ if (SignKeyExchange)
+ {
+ // Create handshake parameters
+ ECDiffieHellmanRSA keyExchange = new ECDiffieHellmanRSA(m_ServerCertificate);
+ m_ClientSignedKeyExchanges.Add(internalClientId, keyExchange);
+
+ // Write public part length
+ m_WriteBuffer[1] = (byte) m_ServerCertificateBytes.Length;
+ m_WriteBuffer[2] = (byte) (m_ServerCertificateBytes.Length >> 8);
+
+ // Write public part of RSA key
+ Buffer.BlockCopy(m_ServerCertificateBytes, 0, m_WriteBuffer, 3, m_ServerCertificateBytes.Length);
+
+ // Get the secure public part (semi heavy)
+ byte[] securePublic = keyExchange.GetSecurePublicPart();
+
+ // Write public part length
+ m_WriteBuffer[3 + m_ServerCertificateBytes.Length] = (byte) securePublic.Length;
+ m_WriteBuffer[4 + m_ServerCertificateBytes.Length] = (byte) (securePublic.Length >> 8);
+
+ // Write key exchange public part
+ Buffer.BlockCopy(securePublic, 0, m_WriteBuffer, 5 + m_ServerCertificateBytes.Length, securePublic.Length);
+
+ // Set length
+ length = 5 + m_ServerCertificateBytes.Length + securePublic.Length;
+ }
+ else
+ {
+ // Create handshake parameters
+ ECDiffieHellman keyExchange = new ECDiffieHellman();
+ m_ClientKeyExchanges.Add(internalClientId, keyExchange);
+
+ // Get the secure public part (semi heavy)
+ byte[] publicKey = keyExchange.GetPublicKey();
+
+ // Write public part length
+ m_WriteBuffer[1] = (byte) publicKey.Length;
+ m_WriteBuffer[2] = (byte) (publicKey.Length >> 8);
+
+ // Write key exchange public part
+ Buffer.BlockCopy(publicKey, 0, m_WriteBuffer, 3, publicKey.Length);
+
+ // Set length
+ length = 3 + publicKey.Length;
+ }
+
+ // Ensure length is set
+ Assert.IsTrue(length != 0);
+
+ // Send hail
+ Transport.Send(internalClientId, new ArraySegment(m_WriteBuffer, 0, length), NetworkDelivery.ReliableSequenced);
+
+ // Add them to client state
+ m_ClientStates.Add(internalClientId, ClientState.WaitingForHailResponse);
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Nothing;
+ }
+ else if (@event == NetworkEvent.Data)
+ {
+ // Keep track of a read head
+ int position = internalPayload.Offset;
+ int start = position;
+
+ MessageType messageType = (MessageType) (internalPayload.Array[position++] & 0x0F);
+
+ if (messageType == MessageType.Hail && !m_IsServer)
+ {
+ // Server sent us a hail
+
+ // Read if the data was signed
+ bool sign = ((internalPayload.Array[start] & 0x80) >> 7) == 1;
+ bool signEveryMessage = ((internalPayload.Array[start] & 0x40) >> 6) == 1;
+
+ if (sign != SignKeyExchange || signEveryMessage != SignEveryMessage)
+ {
+ if (sign != SignKeyExchange)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
+ {
+ NetworkLog.LogErrorServer("Mismatch between " + nameof(SignKeyExchange));
+ }
+ }
+
+ if (signEveryMessage != SignEveryMessage)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
+ {
+ NetworkLog.LogErrorServer("Mismatch between " + nameof(SignEveryMessage));
+ }
+ }
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Nothing;
+ }
+
+ byte[] key;
+
+ if (SignKeyExchange)
+ {
+ // Read certificate length
+ ushort certLength = (ushort)(internalPayload.Array[position++] | (ushort)(internalPayload.Array[position++] << 8));
+
+ // Alloc cert
+ // Cert needs exact buffer size so we cannot reuse
+ byte[] cert = new byte[certLength];
+
+ // Copy cert into cert buffer
+ Buffer.BlockCopy(internalPayload.Array, position += certLength, cert, 0, certLength);
+
+ // Create cert
+ m_ServerCertificate = new X509Certificate2(cert);
+
+ if (ValidateCertificate == null)
+ {
+ Transport.DisconnectLocalClient();
+ throw new Exception("ValidateCertificate handler not set");
+ }
+
+ if (!ValidateCertificate(m_ServerCertificate))
+ {
+ // Failed validation. Disconnect and return
+ Transport.DisconnectLocalClient();
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Nothing;
+ }
+
+ // Create key exchange
+ m_ServerSignedKeyExchange = new ECDiffieHellmanRSA(m_ServerCertificate);
+
+ // Read public part length
+ ushort publicPartLength = (ushort)(internalPayload.Array[position++] | (ushort)(internalPayload.Array[position++] << 8));
+
+ // Read servers public part length
+ byte[] serverPublicPart = new byte[publicPartLength];
+
+ // Copy public part
+ Buffer.BlockCopy(internalPayload.Array, position += publicPartLength, serverPublicPart, 0, publicPartLength);
+
+ // Get shared
+ key = m_ServerSignedKeyExchange.GetVerifiedSharedPart(serverPublicPart);
+ }
+ else
+ {
+ // Create key exchange
+ m_ServerKeyExchange = new ECDiffieHellman();
+
+ // Read public part length
+ ushort publicPartLength = (ushort)(internalPayload.Array[position++] | (ushort)(internalPayload.Array[position++] << 8));
+
+ // Read servers public part length
+ byte[] serverPublicPart = new byte[publicPartLength];
+
+ // Copy buffer
+ Buffer.BlockCopy(internalPayload.Array, position, serverPublicPart, 0, publicPartLength);
+
+ // Get shared
+ key = m_ServerKeyExchange.GetSharedSecretRaw(serverPublicPart);
+ }
+
+ // Do key stretching with PBKDF2-HMAC-SHA1 with Application.productName as salt
+ using (Rfc2898DeriveBytes pbdkf = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(Application.productName), 10_000))
+ {
+ // Add raw key for external use
+ ServerKey = key;
+
+ // ChaCha wants 44 bytes per instance
+ byte[] chaChaData = pbdkf.GetBytes(32 + (12 * 2) + (SignEveryMessage ? 64 : 0));
+
+ // Get key part
+ byte[] chaChaKey = new byte[32];
+ Buffer.BlockCopy(chaChaData, 0, chaChaKey, 0, 32);
+
+ // Get nonce parts
+ byte[] sendNonce = new byte[12];
+ Buffer.BlockCopy(chaChaData, 32, sendNonce, 0, 12);
+
+ byte[] receiveNonce = new byte[12];
+ Buffer.BlockCopy(chaChaData, 32 + 12, receiveNonce, 0, 12);
+
+ // Create cipher
+ m_ServerSendCipher = new ChaCha20Cipher(chaChaKey, sendNonce, 0);
+ m_ServerReceiveCipher = new ChaCha20Cipher(chaChaKey, receiveNonce, 0);
+
+ if (SignEveryMessage)
+ {
+ // Get HMAC key
+ byte[] hmacKey = new byte[64];
+ Buffer.BlockCopy(chaChaData, 32 + (12 * 2), hmacKey, 0, 64);
+
+ // Create HMAC
+ m_ServerAuthenticator = new HMACSHA256(hmacKey);
+ }
+ }
+
+ /* Respond with hail response */
+
+ // Write message type
+ m_WriteBuffer[0] = (byte) MessageType.HailResponse;
+
+ // Get public part to write
+ byte[] publicPart = SignKeyExchange ? m_ServerSignedKeyExchange.GetSecurePublicPart() : m_ServerKeyExchange.GetPublicKey();
+
+ // Write public part length
+ m_WriteBuffer[1] = (byte) publicPart.Length;
+ m_WriteBuffer[2] = (byte) (publicPart.Length >> 8);
+
+ // Write public part
+ Buffer.BlockCopy(publicPart, 0, m_WriteBuffer, 3, publicPart.Length);
+
+ // Send hail response
+ Transport.Send(internalClientId, new ArraySegment(m_WriteBuffer, 0, 3 + publicPart.Length), NetworkDelivery.ReliableSequenced);
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Nothing;
+ }
+ else if (messageType == MessageType.HailResponse && m_IsServer && m_ClientStates.ContainsKey(internalClientId) && m_ClientStates[internalClientId] == ClientState.WaitingForHailResponse)
+ {
+ // Client sent us a hail response
+
+ // Read clients public part
+ ushort clientPublicPartLength = (ushort)(internalPayload.Array[position++] | (ushort)(internalPayload.Array[position++] << 8));
+
+ // Alloc public part
+ byte[] clientPublicPart = new byte[clientPublicPartLength];
+
+ // Copy public part
+ Buffer.BlockCopy(internalPayload.Array, position, clientPublicPart, 0, clientPublicPartLength);
+
+ byte[] key;
+
+ if (SignKeyExchange)
+ {
+ // Get key
+ key = m_ClientSignedKeyExchanges[internalClientId].GetVerifiedSharedPart(clientPublicPart);
+ }
+ else
+ {
+ // Get key
+ key = m_ClientKeyExchanges[internalClientId].GetSharedSecretRaw(clientPublicPart);
+ }
+
+ // Do key stretching with PBKDF2-HMAC-SHA1 with Application.productName as salt
+ using (Rfc2898DeriveBytes pbdkf = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(Application.productName), 10_000))
+ {
+ // Add raw key for external use
+ ClientKeys.Add(internalClientId, key);
+
+ // ChaCha wants 44 bytes per instance
+ byte[] chaChaData = pbdkf.GetBytes(32 + (12 * 2) + (SignEveryMessage ? 64 : 0));
+
+ // Get key part
+ byte[] chaChaKey = new byte[32];
+ Buffer.BlockCopy(chaChaData, 0, chaChaKey, 0, 32);
+
+ // Get nonce part
+ byte[] chaChaSendNonce = new byte[12];
+ Buffer.BlockCopy(chaChaData, 32 + 12, chaChaSendNonce, 0, 12);
+
+ byte[] chaChaReceiveNonce = new byte[12];
+ Buffer.BlockCopy(chaChaData, 32, chaChaReceiveNonce, 0, 12);
+
+ // Create cipher
+ m_ClientSendCiphers.Add(internalClientId, new ChaCha20Cipher(chaChaKey, chaChaSendNonce, 0));
+ m_ClientReceiveCiphers.Add(internalClientId, new ChaCha20Cipher(chaChaKey, chaChaReceiveNonce, 0));
+
+ if (SignEveryMessage)
+ {
+ // Get HMAC key
+ byte[] hmacKey = new byte[64];
+ Buffer.BlockCopy(chaChaData, 32 + (12 * 2), hmacKey, 0, 64);
+
+ // Create HMAC
+ m_ClientAuthenticators.Add(internalClientId, new HMACSHA256(hmacKey));
+ }
+ }
+
+ // Cleanup
+ m_ClientSignedKeyExchanges.Remove(internalClientId);
+
+ /* Respond with ready response */
+
+ // Write message type
+ m_WriteBuffer[0] = (byte) MessageType.Ready;
+
+ // Send ready message
+ Transport.Send(internalClientId, new ArraySegment(m_WriteBuffer, 0, 1), NetworkDelivery.ReliableSequenced);
+
+ // Elevate to connected
+ m_ClientStates[internalClientId] = ClientState.Connected;
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Connect;
+ }
+ else if (messageType == MessageType.Ready && !m_IsServer)
+ {
+ // Server is ready for us!
+ // Let the MLAPI know we are connected
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Connect;
+ }
+ else if (messageType == MessageType.Internal && (!m_IsServer || (m_ClientStates.ContainsKey(internalClientId) && m_ClientStates[internalClientId] == ClientState.Connected)))
+ {
+ // Decrypt and pass message to the MLAPI
+
+ if (SignEveryMessage)
+ {
+ // Copy auth code from last 32 bytes
+ Buffer.BlockCopy(internalPayload.Array, (internalPayload.Count + internalPayload.Offset) - 32, m_AuthenticationBuffer, 0, 32);
+
+ // Get authenticator
+ HMACSHA256 authenticator = m_IsServer ? m_ClientAuthenticators[internalClientId] : m_ServerAuthenticator;
+
+ // Calculate signature
+ byte[] signature = authenticator.ComputeHash(internalPayload.Array, internalPayload.Offset, internalPayload.Count - 32);
+
+ // Compare
+ if (!ComparisonUtils.ConstTimeArrayEqual(signature, m_AuthenticationBuffer))
+ {
+ // HMAC IS INVALID
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
+ {
+ NetworkLog.LogErrorServer("HMAC did not match for message. It has been dropped");
+ }
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Nothing;
+ }
+ }
+
+ // Get the least significant bits of the counter
+ byte leastSignificantBitsCounter = internalPayload.Array[position++];
+
+ // Get the correct cipher
+ ChaCha20Cipher cipher = m_IsServer ? m_ClientReceiveCiphers[internalClientId] : m_ServerReceiveCipher;
+
+ // Get the correct counter
+ uint counter = m_IsServer ? m_ClientReceiveCiphers[internalClientId].Counter : m_ServerReceiveCipher.Counter;
+
+ if ((counter & 0xFF) != leastSignificantBitsCounter)
+ {
+ /* COUNTER PREDICTION */
+ // The goal of this code is to predict the senders counter.
+ // This must work.
+ // The only thing sent is the 8 least significant bytes of the sender counter.
+ // The rest has to be predicted.
+ // - TwoTen
+
+ if ((counter & 0xFF) > byte.MaxValue - 64 && leastSignificantBitsCounter < 64)
+ {
+ // The counter was on the upper half of the bits and the message we got was rolled over
+
+ // Roll over the counter then set the 8 least significant bits to the senders
+ counter = ((counter + ((uint)byte.MaxValue - ((byte) counter)) + 1) & 0xFFFFFF00) | leastSignificantBitsCounter;
+ }
+ else if ((counter & 0xFF) < 64 && leastSignificantBitsCounter > byte.MaxValue - 64)
+ {
+ // The counter was on the lower half of the bits and the message we got was in the past
+
+ // Roll the counter back to make the least significant bits 0xFF.
+
+ // TODO: Optimize with something closer to real bitmath
+ // Eg. https://bisqwit.iki.fi/story/howto/bitmath/
+ while ((counter & 0xFF) != byte.MaxValue)
+ {
+ counter--;
+ }
+
+ // Counter was rolled back to where the bits were on the upper half then set to the least significant bits of the sender
+ counter = (counter & 0xFFFFFF00) | leastSignificantBitsCounter;
+ }
+ else
+ {
+ // We didn't detect a integer rollover from the 8 least significant bits. We simply set the last 8 to the senders 8 lsb
+ counter = (counter & 0xFFFFFF00) | leastSignificantBitsCounter;
+ }
+
+ // Reset the cipher nonce and IV to the predicted value
+ cipher.SetCounter(counter);
+ }
+
+ // Decrypt bytes
+ cipher.ProcessBytes(internalPayload.Array, position, m_CryptoBuffer, 0, (internalPayload.Count - position) - (SignEveryMessage ? 32 : 0));
+
+ clientId = internalClientId;
+ payload = new ArraySegment(m_CryptoBuffer, 0, (internalPayload.Count - position) - (SignEveryMessage ? 32 : 0));
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Data;
+ }
+ }
+ else if (@event == NetworkEvent.Disconnect)
+ {
+ // Cleanup
+
+ if (m_IsServer)
+ {
+ if (SignKeyExchange)
+ {
+ if (m_ClientSignedKeyExchanges.ContainsKey(internalClientId))
+ {
+ m_ClientSignedKeyExchanges.Remove(internalClientId);
+ }
+ }
+ else
+ {
+ if (m_ClientKeyExchanges.ContainsKey(internalClientId))
+ {
+ m_ClientKeyExchanges.Remove(internalClientId);
+ }
+ }
+
+ if (ClientKeys.ContainsKey(internalClientId))
+ {
+ ClientKeys.Remove(internalClientId);
+ }
+
+ if (m_ClientSendCiphers.ContainsKey(internalClientId))
+ {
+ m_ClientSendCiphers[internalClientId].Dispose();
+ m_ClientSendCiphers.Remove(internalClientId);
+ }
+
+ if (m_ClientReceiveCiphers.ContainsKey(internalClientId))
+ {
+ m_ClientReceiveCiphers[internalClientId].Dispose();
+ m_ClientReceiveCiphers.Remove(internalClientId);
+ }
+
+ if (m_ClientStates.ContainsKey(internalClientId))
+ {
+ m_ClientStates.Remove(internalClientId);
+ }
+
+ if (m_ClientAuthenticators.ContainsKey(internalClientId))
+ {
+ m_ClientAuthenticators[internalClientId].Dispose();
+ m_ClientAuthenticators.Remove(internalClientId);
+ }
+ }
+ else
+ {
+ m_ServerSignedKeyExchange = null;
+ m_ServerKeyExchange = null;
+ ServerKey = null;
+ }
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = internalReceiveTime;
+ return NetworkEvent.Disconnect;
+ }
+
+ clientId = internalClientId;
+ payload = new ArraySegment();
+ receiveTime = 0;
+ return NetworkEvent.Nothing;
+ }
+
+ public override bool StartClient()
+ {
+ m_IsServer = false;
+ return Transport.StartClient();
+ }
+
+ public override bool StartServer()
+ {
+ m_IsServer = true;
+ ParsePFX();
+ return Transport.StartServer();
+ }
+
+ public override void DisconnectRemoteClient(ulong clientId)
+ {
+ Transport.DisconnectRemoteClient(clientId);
+ }
+
+ public override void DisconnectLocalClient()
+ {
+ Transport.DisconnectLocalClient();
+ }
+
+ public override ulong GetCurrentRtt(ulong clientId)
+ {
+ return Transport.GetCurrentRtt(clientId);
+ }
+
+ public override void Shutdown()
+ {
+ Transport.Shutdown();
+ }
+
+ public override void Initialize()
+ {
+ Transport.Initialize();
+ }
+
+ private void ParsePFX()
+ {
+ try
+ {
+ string pfx = ServerBase64PFX.Trim();
+
+ try
+ {
+ if (m_IsServer && SignKeyExchange && !string.IsNullOrWhiteSpace(pfx))
+ {
+ byte[] decodedPfx = Convert.FromBase64String(ServerBase64PFX);
+
+ m_ServerCertificate = new X509Certificate2(decodedPfx);
+
+ if (!m_ServerCertificate.HasPrivateKey)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Normal)
+ {
+ NetworkLog.LogWarningServer("The imported PFX file did not have a private key");
+ }
+ }
+ }
+ }
+ catch (FormatException e)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
+ {
+ NetworkLog.LogErrorServer("Parsing PFX failed: " + e);
+ }
+ }
+ }
+ catch (CryptographicException e)
+ {
+ if (NetworkLog.CurrentLogLevel <= LogLevel.Error)
+ {
+ NetworkLog.LogErrorServer("Importing of certificate failed: " + e);
+ }
+ }
+ }
+ }
+}
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/CryptographyTransportAdapter.cs.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/CryptographyTransportAdapter.cs.meta
new file mode 100644
index 00000000..d7435c0a
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/CryptographyTransportAdapter.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d1a5905f8f5d420c8806675d3843373a
+timeCreated: 1617961447
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.dll b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.dll
new file mode 100644
index 00000000..e2cfc9e3
Binary files /dev/null and b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.dll differ
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.dll.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.dll.meta
new file mode 100644
index 00000000..29711126
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.dll.meta
@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 48697e5b7b0606b0fb2cd521724ff7a8
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 1
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ Any:
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ DefaultValueInitialized: true
+ - first:
+ Windows Store Apps: WindowsStoreApps
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.pdb.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.pdb.meta
new file mode 100644
index 00000000..d89b8feb
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/MLAPI.Cryptography.pdb.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a5005a3014db8cb0dbcce77f9f0c5d28
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/com.mlapi.contrib.transport.chacha20.asmdef b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/com.mlapi.contrib.transport.chacha20.asmdef
new file mode 100644
index 00000000..9ef20792
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/com.mlapi.contrib.transport.chacha20.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "ChaCha20 MLAPI Transport",
+ "rootNamespace": "MLAPI.Transport.ChaCha20",
+ "references": [
+ "Unity.Multiplayer.MLAPI.Runtime"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/Runtime/com.mlapi.contrib.transport.chacha20.asmdef.meta b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/com.mlapi.contrib.transport.chacha20.asmdef.meta
new file mode 100644
index 00000000..6bb71d21
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/Runtime/com.mlapi.contrib.transport.chacha20.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 2aa43b0c797e0d444a7e82e75d4f2082
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/package.json b/Transports/com.mlapi.contrib.transport.chacha20/package.json
new file mode 100644
index 00000000..7b5520ff
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "com.mlapi.contrib.transport.chacha20",
+ "displayName": "ChaCha20 Transport for MLAPI",
+ "version": "1.0.0",
+ "unity": "2019.4",
+ "description": "ChaCha20 Transport for MLAPI",
+ "author": "Albin Corén",
+ "dependencies": {
+ }
+}
diff --git a/Transports/com.mlapi.contrib.transport.chacha20/package.json.meta b/Transports/com.mlapi.contrib.transport.chacha20/package.json.meta
new file mode 100644
index 00000000..bb870926
--- /dev/null
+++ b/Transports/com.mlapi.contrib.transport.chacha20/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f719a183491e7c849a3308ea58b10205
+PackageManifestImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: