using System.Collections.Immutable; using System.Security.Cryptography; using System.Runtime.Intrinsics; using System.Runtime.CompilerServices; // For MethodImplOptions.AggressiveInlining namespace ViewModels; /// /// Class that can be used for AES CTR encryption / decryption /// public sealed class AES_CTR : IDisposable { /// /// What are allowed key lengths in bytes (128, 192 and 256 bits) /// /// public static readonly ImmutableArray allowedKeyLengths = [16, 24, 32]; /// /// What is allowed initial counter length in bytes /// public const int allowedCounterLength = 16; /// /// Only allowed Initialization vector length in bytes /// private const int ivLength = 16; /// /// How many bytes are processed at time /// private const int processBytesAtTime = 16; /// /// Internal counter /// private readonly byte[] counter = new byte[allowedCounterLength]; /// /// Internal transformer for doing encrypt/decrypt transforming /// private readonly ICryptoTransform counterEncryptor; /// /// Determines if the objects in this class have been disposed of. Set to true by the Dispose() method. /// private bool isDisposed; /// /// Changes counter behaviour according endianess. /// private readonly bool isLittleEndian; /// /// AES_CTR constructor /// /// Key as readonlyspan. (128, 192 or 256 bits) /// Initial counter as readonlyspan. 16 bytes /// Is initial counter little endian (default false) /// public AES_CTR(ReadOnlySpan key, ReadOnlySpan initialCounter, bool littleEndian = false) : this(key.ToArray(), initialCounter.ToArray(), littleEndian) { } /// /// AES_CTR constructor /// /// Key as byte array. (128, 192 or 256 bits) /// Initial counter as byte array. 16 bytes /// Is initial counter little endian (default false) public AES_CTR(byte[] key, byte[] initialCounter, bool littleEndian = false) { if (key == null) { throw new ArgumentNullException("Key is null"); } if (!allowedKeyLengths.Contains(key.Length)) { throw new ArgumentException($"Key length must be either {allowedKeyLengths[0]}, {allowedKeyLengths[1]} or {allowedKeyLengths[2]} bytes. Actual: {key.Length}"); } if (initialCounter == null) { throw new ArgumentNullException("Initial counter is null"); } if (allowedCounterLength != initialCounter.Length) { throw new ArgumentException($"Initial counter must be {allowedCounterLength} bytes"); } this.isDisposed = false; SymmetricAlgorithm aes = Aes.Create(); aes.Mode = CipherMode.ECB; aes.Padding = PaddingMode.None; // Create copy of initial counter since state is kept during the lifetime of AES_CTR Buffer.BlockCopy(initialCounter, 0, this.counter, 0, allowedCounterLength); this.isLittleEndian = littleEndian; // Initialization vector is always full of zero bytes in CTR mode var zeroIv = new byte[ivLength]; this.counterEncryptor = aes.CreateEncryptor(key, zeroIv); } #region Encrypt /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array /// Number of bytes to encrypt /// Use SIMD (true by default) public void EncryptBytes(byte[] output, byte[] input, int numBytes, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (output == null) { throw new ArgumentNullException("output", "Output cannot be null"); } if (numBytes < 0 || numBytes > input.Length) { throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]"); } if (output.Length < numBytes) { throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}"); } this.WorkBytes(output, input, numBytes, useSIMD); } /// /// Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 /// Use SIMD (true by default) public void EncryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, bool useSIMD = true) { this.WorkStreams(output, input, useSIMD, howManyBytesToProcessAtTime); } /// /// Async encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 /// Use SIMD (true by default) /// public async Task EncryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, bool useSIMD = true) { await this.WorkStreamsAsync(output, input, useSIMD, howManyBytesToProcessAtTime); } /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array /// Use SIMD (true by default) public void EncryptBytes(byte[] output, byte[] input, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (output == null) { throw new ArgumentNullException("output", "Output cannot be null"); } this.WorkBytes(output, input, input.Length, useSIMD); } /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Number of bytes to encrypt /// Use SIMD (true by default) /// Byte array that contains encrypted bytes public byte[] EncryptBytes(byte[] input, int numBytes, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (numBytes < 0 || numBytes > input.Length) { throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]"); } byte[] returnArray = new byte[numBytes]; this.WorkBytes(returnArray, input, numBytes, useSIMD); return returnArray; } /// /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Use SIMD (true by default) /// Byte array that contains encrypted bytes public byte[] EncryptBytes(byte[] input, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } byte[] returnArray = new byte[input.Length]; this.WorkBytes(returnArray, input, input.Length, useSIMD); return returnArray; } /// /// Encrypt string as UTF8 byte array, returns byte array that is allocated by method. /// /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform /// Input string /// Use SIMD (true by default) /// Byte array that contains encrypted bytes public byte[] EncryptString(string input, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input); byte[] returnArray = new byte[utf8Bytes.Length]; this.WorkBytes(returnArray, utf8Bytes, utf8Bytes.Length, useSIMD); return returnArray; } #endregion // Encrypt #region Decrypt /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array /// Number of bytes to encrypt /// Use SIMD (true by default) public void DecryptBytes(byte[] output, byte[] input, int numBytes, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (output == null) { throw new ArgumentNullException("output", "Output cannot be null"); } if (numBytes < 0 || numBytes > input.Length) { throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]"); } if (output.Length < numBytes) { throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}"); } this.WorkBytes(output, input, numBytes, useSIMD); } /// /// Decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 /// Use SIMD (true by default) public void DecryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, bool useSIMD = true) { this.WorkStreams(output, input, useSIMD, howManyBytesToProcessAtTime); } /// /// Async decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// How many bytes to read and write at time, default is 1024 /// Use SIMD (true by default) /// public async Task DecryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024, bool useSIMD = true) { await this.WorkStreamsAsync(output, input, useSIMD, howManyBytesToProcessAtTime); } /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Output byte array, must have enough bytes /// Input byte array /// Use SIMD (true by default) public void DecryptBytes(byte[] output, byte[] input, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (output == null) { throw new ArgumentNullException("output", "Output cannot be null"); } this.WorkBytes(output, input, input.Length, useSIMD); } /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Number of bytes to encrypt /// Use SIMD (true by default) /// Byte array that contains decrypted bytes public byte[] DecryptBytes(byte[] input, int numBytes, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } if (numBytes < 0 || numBytes > input.Length) { throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]"); } byte[] returnArray = new byte[numBytes]; this.WorkBytes(returnArray, input, numBytes, useSIMD); return returnArray; } /// /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. /// /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method /// Input byte array /// Use SIMD (true by default) /// Byte array that contains decrypted bytes public byte[] DecryptBytes(byte[] input, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } byte[] returnArray = new byte[input.Length]; this.WorkBytes(returnArray, input, input.Length, useSIMD); return returnArray; } /// /// Decrypt UTF8 byte array to string. /// /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform /// Byte array /// Use SIMD (true by default) /// Byte array that contains encrypted bytes public string DecryptUTF8ByteArray(byte[] input, bool useSIMD = true) { if (input == null) { throw new ArgumentNullException("input", "Input cannot be null"); } byte[] tempArray = new byte[input.Length]; this.WorkBytes(tempArray, input, input.Length, useSIMD); return System.Text.Encoding.UTF8.GetString(tempArray); } #endregion // Decrypt /// /// Decrypt / Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) /// /// Output stream /// Input stream /// Use SIMD (true by default) /// How many bytes to read and write at time, default is 1024 private void WorkStreams(Stream output, Stream input, bool useSIMD, int howManyBytesToProcessAtTime = 1024) { int readBytes; byte[] inputBuffer = new byte[howManyBytesToProcessAtTime]; byte[] outputBuffer = new byte[howManyBytesToProcessAtTime]; while ((readBytes = input.Read(inputBuffer, 0, howManyBytesToProcessAtTime)) > 0) { // Encrypt or decrypt this.WorkBytes(output: outputBuffer, input: inputBuffer, numBytes: readBytes, useSIMD); // Write buffer output.Write(outputBuffer, 0, readBytes); } } private async Task WorkStreamsAsync(Stream output, Stream input, bool useSIMD, int howManyBytesToProcessAtTime = 1024) { byte[] readBytesBuffer = new byte[howManyBytesToProcessAtTime]; byte[] writeBytesBuffer = new byte[howManyBytesToProcessAtTime]; int howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime); while (howManyBytesWereRead > 0) { // Encrypt or decrypt this.WorkBytes(output: writeBytesBuffer, input: readBytesBuffer, numBytes: howManyBytesWereRead, useSIMD); // Write await output.WriteAsync(writeBytesBuffer, 0, howManyBytesWereRead); // Read more howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime); } } private void WorkBytes(byte[] output, byte[] input, int numBytes, bool useSIMD) { if (isDisposed) { throw new ObjectDisposedException("state", "AES_CTR has already been disposed"); } int offset = 0; var tmp = new byte[allowedCounterLength]; int howManyFullLoops = numBytes / processBytesAtTime; int tailByteCount = numBytes - (howManyFullLoops * processBytesAtTime); for (int loop = 0; loop < howManyFullLoops; loop++) { // Generate new XOR mask for next processBytesAtTime this.counterEncryptor.TransformBlock(counter, 0, allowedCounterLength, tmp, 0); this.IncreaseCounter(); if (useSIMD) { // 1 x 16 bytes Vector128 inputV = Vector128.Create(input, offset); Vector128 tmpV = Vector128.Create(tmp, 0); Vector128 outputV = inputV ^ tmpV; outputV.CopyTo(output, offset); } else { for (int i = 0; i < processBytesAtTime; i++) { output[i + offset] = (byte)(input[i + offset] ^ tmp[i]); } } offset += processBytesAtTime; } // In case there are some bytes left if (tailByteCount > 0) { // Generate new XOR mask for next processBytesAtTime this.counterEncryptor.TransformBlock(counter, 0, allowedCounterLength, tmp, 0); this.IncreaseCounter(); for (int i = 0; i < tailByteCount; i++) { output[i + offset] = (byte)(input[i + offset] ^ tmp[i]); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void IncreaseCounter() { // Increase counter (basically this increases the last index first and continues to one before that if 255 -> 0, better solution would be to use uint128, but it does not exist yet) if (this.isLittleEndian) { // LittleEndian for (int i = 0; i < allowedCounterLength; i++) { if (++counter[i] != 0) { break; } } } else { // BigEndian for (int i = allowedCounterLength - 1; i >= 0; i--) { if (++counter[i] != 0) { break; } } } } /// /// ÈçºÎʹÓà /// void test() { byte[] mySimpleTextAsBytes = System.Text.Encoding.ASCII.GetBytes("CM:GPIO_WRITE:5:0x19:0x01:1sdfghasada\r\n"); // example data var IV = new byte[] { 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff }; byte[] key = new byte[] { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c }; // Encrypt AES_CTR forEncrypting = new AES_CTR(key, IV); byte[] encryptedContent = new byte[mySimpleTextAsBytes.Length]; forEncrypting.EncryptBytes(encryptedContent, mySimpleTextAsBytes); // Decrypt AES_CTR forDecrypting = new AES_CTR(key, IV); byte[] decryptedContent = new byte[encryptedContent.Length]; forDecrypting.DecryptBytes(decryptedContent, encryptedContent); Console.WriteLine("Hello, World!"); } #region Destructor and Disposer /// /// Clear and dispose of the internal variables. The finalizer is only called if Dispose() was never called on this cipher. /// ~AES_CTR() { 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 (!isDisposed) { if (disposing) { /* Cleanup managed objects by calling their Dispose() methods */ if (this.counterEncryptor != null) { this.counterEncryptor.Dispose(); } } /* Cleanup here */ Array.Clear(this.counter, 0, allowedCounterLength); } isDisposed = true; } #endregion // Destructor and Disposer }