6
\$\begingroup\$

Would anyone care to elaborate my approach to a multi-layer encryption scheme. It encrypts with four layers and gets shuffled with a random key as well. This is used for a password vault application.

    /// <summary>
    ///     Encrypts the contents of a file using Argon2 key derivation and four layers of encryption.
    /// </summary>
    /// <param name="userName">The username associated with the user's salt for key derivation.</param>
    /// <param name="passWord">The user's password used for key derivation.</param>
    /// <param name="plainText">The plaintext to encrypt.</param>
    /// <returns>
    ///     A Task that completes with the encrypted content of the specified file.
    ///     If any error occurs during the process, returns an empty byte array.
    /// </returns>
    /// <remarks>
    ///     This method performs the following steps:
    ///     1. Validates input parameters to ensure they are not null or empty.
    ///     2. Retrieves the user-specific salt for key derivation.
    ///     3. Derives an encryption key from the user's password and the obtained salt using Argon2id.
    ///     4. Extracts key components for encryption, including two keys and an HMAC key.
    ///     5. Reads and encodes the content of the specified file.
    ///     6. Encrypts the file content using XChaCha20-Poly1305 encryption.
    ///     7. Clears sensitive information, such as the user's password, from memory.
    /// </remarks>
    public static async Task<byte[]> EncryptFile(string userName, byte[] passWord, string plainText)
    {
        if (string.IsNullOrEmpty(userName))
            throw new ArgumentException("Value was empty.", nameof(userName));
        if (passWord.Length == 0)
            throw new ArgumentException("Value was empty.", nameof(passWord));
        if (string.IsNullOrEmpty(plainText))
            throw new ArgumentException("Value was empty.", nameof(plainText));

        var salt = Authentication.GetUserSalt(userName);

        var bytes = await HashingMethods.Argon2Id(passWord, salt, 544);
        if (bytes == Array.Empty<byte>())
            throw new Exception("Value was empty.");

        var fileBytes = DataConversionHelpers.StringToByteArray(await File.ReadAllTextAsync(plainText));

        if (fileBytes == null || fileBytes.Length == 0 || salt == null || salt.Length == 0)
            throw new ArgumentException("Value was empty.");

        var (key, key2, key3, key4, key5, hMacKey, hMacKey2, hMacKey3) = BufferInit.InitBuffers(bytes);

        var compressedText = await CryptoUtilities.CompressText(fileBytes);
        var encryptedFile = EncryptionDecryption.EncryptV3(ref compressedText, ref key, ref key2, ref key3, ref key4,
            ref key5, ref hMacKey,
            ref hMacKey2, ref hMacKey3);

        var arrays = new[]
        {
            key, key2, key3, key4, key5, hMacKey, hMacKey2, hMacKey3, bytes
        };

        CryptoUtilities.ClearMemory(arrays);

        return encryptedFile;
    }

    /// <summary>
    ///     Decrypts the contents of an encrypted file using Argon2 key derivation and four layers of decryption.
    /// </summary>
    /// <param name="userName">The username associated with the user's salt for key derivation.</param>
    /// <param name="passWord">The user's password used for key derivation.</param>
    /// <param name="cipherText">The ciphertext to decrypt.</param>
    /// <returns>
    ///     A Task that completes with the decrypted content of the specified encrypted file.
    ///     If any error occurs during the process, returns an empty byte array.
    /// </returns>
    /// <remarks>
    ///     This method performs the following steps:
    ///     1. Validates input parameters to ensure they are not null or empty.
    ///     2. Retrieves the user-specific salts for key derivation.
    ///     3. Derives an encryption key from the user's password and the obtained salt using Argon2id.
    ///     4. Extracts key components for decryption, including two keys and an HMAC key.
    ///     5. Reads and decodes the content of the encrypted file.
    ///     6. Decrypts the file content using ChaCha20-Poly1305 decryption.
    ///     7. Clears sensitive information, such as the user's password, from memory.
    /// </remarks>
    public static async Task<byte[]> DecryptFile(string userName, byte[] passWord, string cipherText)
    {
        if (string.IsNullOrEmpty(userName))
            throw new ArgumentException("Value was empty.", nameof(userName));
        if (passWord.Length == 0)
            throw new ArgumentException("Value was empty.", nameof(passWord));
        if (string.IsNullOrEmpty(cipherText))
            throw new ArgumentException("Value was empty.", nameof(cipherText));

        var salt = Authentication.GetUserSalt(userName);

        var bytes = await HashingMethods.Argon2Id(passWord, salt, 544);
        var (key, key2, key3, key4, key5, hMacKey, hMacKey2, hMacKey3) = BufferInit.InitBuffers(bytes);

        var fileStr = await File.ReadAllTextAsync(cipherText);
        var fileBytes = DataConversionHelpers.Base64StringToByteArray(fileStr);

        if (fileBytes == Array.Empty<byte>() || salt == Array.Empty<byte>())
            throw new ArgumentException("Value was empty.");

        var decryptedFile =
            EncryptionDecryption.DecryptV3(ref fileBytes, ref key, ref key2, ref key3, ref key4, ref key5, ref hMacKey,
                ref hMacKey2, ref hMacKey3);
        var decompressedText = await CryptoUtilities.DecompressText(decryptedFile);

        var arrays = new[]
        {
            key, key2, key3, key4, key5, hMacKey, hMacKey2, hMacKey3, bytes
        };
        CryptoUtilities.ClearMemory(arrays);

        return decompressedText;
    }
    private static partial class Memset
    {
        [LibraryImport("msvcrt.dll", EntryPoint = "memset", SetLastError = false)]
        [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
        public static partial void MemSet(IntPtr dest, int c, int byteCount);
    }
    /// <summary>
    ///     Utility class for cryptographic settings and initialization.
    /// </summary>
    public static class CryptoConstants
    {
        public const int SaltSize = 128;
        public const int HmacLength = 64;
        public const int ChaChaNonceSize = 24;
        public const int KeySize = 32;
        public const int Iv = 16;
        public const int ThreeFish = 128;
        public const int ShuffleKey = 128;
        public const int BlockBitSize = 128;
        public const int KeyBitSize = 256;
        public static int Iterations = Settings.Default.Iterations;
        public static double MemorySize = Settings.Default.MemorySize * Math.Pow(1024, 2);
        public static int Parallelism = Settings.Default.Parallelism;

        public static readonly RandomNumberGenerator RndNum = RandomNumberGenerator.Create();
        public static byte[] SecurePasswordSalt { get; set; } = [];
#pragma warning disable CA2211

        public static byte[] Hash = [];
        public static byte[] SecurePassword = [];

#pragma warning restore CA2211
    }
    public static class HashingMethods
    {
        /// <summary>
        ///     Hashes a password inside a char array or derives a key from a password.
        /// </summary>
        /// <param name="passWord">The char array to hash.</param>
        /// <param name="salt">The salt used during the argon2id hashing process.</param>
        /// <param name="outputSize">The output size in bytes.</param>
        /// <returns>Either a derived key or password hash byte array.</returns>
        public static async Task<byte[]> Argon2Id(byte[] passWord, byte[] salt, int outputSize)
        {
            if (passWord == null || passWord.Length == 0)
                throw new ArgumentException("Password cannot be null or empty.", nameof(passWord));
            if (salt == null || salt.Length == 0)
                throw new ArgumentException("Salt cannot be null or empty.", nameof(salt));

            using var argon2 = new Argon2id(passWord);
            argon2.Salt = salt;
            argon2.DegreeOfParallelism = CryptoConstants.Parallelism;
            argon2.Iterations = CryptoConstants.Iterations;
            argon2.MemorySize = (int)CryptoConstants.MemorySize;

            var result = await argon2.GetBytesAsync(outputSize);

            return result;
        }

        /// <summary>
        ///     Computes the Hash-based Message Authentication Code (HMAC) using the SHA3-512 algorithm.
        /// </summary>
        /// <param name="input">The byte array to be authenticated.</param>
        /// <param name="key">The key used for HMAC computation.</param>
        /// <returns>The HMAC-SHA3-512 authentication code as a byte array.</returns>
        public static byte[] HmacSha3(byte[] input, byte[] key)
        {
            var digest = new Sha3Digest(512);

            var hMac = new HMac(digest);
            hMac.Init(new KeyParameter(key));

            hMac.BlockUpdate(input, 0, input.Length);

            var result = new byte[hMac.GetMacSize()];
            hMac.DoFinal(result, 0);

            return result;
        }

        /// <summary>
        ///     Computes the SHA3-512 hash for the given input byte array.
        /// </summary>
        /// <param name="input">The input byte array for which the hash needs to be computed.</param>
        /// <returns>A byte array representing the SHA3-512 hash of the input.</returns>
        public static byte[] Sha3Hash(byte[] input)
        {
            var digest = new Sha3Digest(512);

            digest.BlockUpdate(input, 0, input.Length);
            var result = new byte[digest.GetDigestSize()];
            digest.DoFinal(result, 0);

            return result;
        }
    }
    public static class CryptoUtilities
    {
        /// <summary>
        ///     Generates a random integer using a cryptographically secure random number generator.
        /// </summary>
        /// <returns>A random integer value.</returns>
        /// <remarks>
        ///     This method generates a random integer by obtaining random bytes from a cryptographically secure
        ///     random number generator and converting them to an integer using the BitConverter class.
        /// </remarks>
        private static int RndInt()
        {
            var buffer = new byte[sizeof(int)];

            CryptoConstants.RndNum.GetBytes(buffer);

            var result = BitConverter.ToInt32(buffer, 0);

            return result;
        }

        /// <summary>
        ///     Generates a random integer within the specified inclusive range.
        /// </summary>
        /// <param name="min">The minimum value of the range (inclusive).</param>
        /// <param name="max">The maximum value of the range (inclusive).</param>
        /// <returns>A random integer within the specified range.</returns>
        /// <exception cref="ArgumentException">
        ///     Thrown if the specified minimum value is greater than or equal to the maximum
        ///     value.
        /// </exception>
        /// <remarks>
        ///     This method generates a random integer within the specified inclusive range using the RndInt method.
        ///     The generated integer is constrained to the provided range by applying modular arithmetic.
        /// </remarks>
        public static int BoundedInt(int min, int max)
        {
            if (min >= max)
                throw new ArgumentException("Min must be less than max.");

            var value = RndInt();

            var range = max - min;

            var result = min + Math.Abs(value) % range;

            return result;
        }

        /// <summary>
        ///     Generates a random byte array of the specified size using a cryptographically secure random number generator.
        /// </summary>
        /// <param name="size">The size of the byte array to generate.</param>
        /// <returns>A random byte array of the specified size.</returns>
        /// <remarks>
        ///     This method generates a random byte array by obtaining random bytes from a cryptographically secure
        ///     random number generator. The size of the byte array is determined by the input parameter 'size'.
        /// </remarks>
        public static byte[] RndByteSized(int size)
        {
            var buffer = new byte[size];

            CryptoConstants.RndNum.GetBytes(buffer);

            return buffer;
        }

        /// <summary>
        ///     Compares two password hashes in a secure manner using fixed-time comparison.
        /// </summary>
        /// <param name="hash1">The first password hash.</param>
        /// <param name="hash2">The second password hash.</param>
        /// <returns>
        ///     A bool that completes with 'true' if the hashes are equal, and 'false' otherwise.
        /// </returns>
        /// <exception cref="ArgumentException">
        ///     Thrown if either hash is null or empty.
        /// </exception>
        /// <remarks>
        ///     This method uses fixed-time comparison to mitigate certain types of timing attacks.
        /// </remarks>
        public static bool ComparePassword(byte[] hash1, byte[] hash2)
        {
            if (hash1.Length == 0 || hash1 == null)
                throw new ArgumentException("Value was empty or null.", nameof(hash1));
            if (hash2.Length == 0 || hash2 == null)
                throw new ArgumentException("Value was empty or null.", nameof(hash2));

            return CryptographicOperations.FixedTimeEquals(hash1, hash2);
        }

        /// <summary>
        ///     Clears the sensitive information stored in one or more byte arrays using memset.
        /// </summary>
        /// <remarks>
        ///     This method uses a pinned array and the SecureMemoryClear function to overwrite the memory
        ///     containing sensitive information, enhancing security by preventing the information from being
        ///     easily accessible in memory.
        /// </remarks>
        /// <param name="arrays">The byte arrays containing sensitive information to be cleared.</param>
        /// <exception cref="ArgumentNullException">Thrown if any of the input arrays is null.</exception>
        public static void ClearMemory(byte[][] arrays)
        {
            if (arrays == null)
                throw new ArgumentNullException(nameof(arrays), "Input cannot be null.");

            foreach (var byteArray in arrays)
            {
                var handle = GCHandle.Alloc(byteArray, GCHandleType.Pinned);

                try
                {
                    Memset.MemSet(handle.AddrOfPinnedObject(), 0, byteArray.Length);
                }
                catch (AccessViolationException ex)
                {
                    ErrorLogging.ErrorLog(ex);
                    MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    handle.Free();

                    GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
                    GC.WaitForPendingFinalizers();
                }
            }
        }

        public static void ClearMemory(byte[] array)
        {
            var handle = GCHandle.Alloc(array, GCHandleType.Pinned);

            try
            {
                Memset.MemSet(handle.AddrOfPinnedObject(), 0, array.Length * sizeof(byte));
            }
            catch (AccessViolationException ex)
            {
                ErrorLogging.ErrorLog(ex);
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                handle.Free();
            }
        }

        /// <summary>
        ///     Clears the sensitive information stored in one or more char arrays securely.
        /// </summary>
        /// <remarks>
        ///     This method uses a pinned array and the SecureMemoryClear function to overwrite the memory
        ///     containing sensitive information, enhancing security by preventing the information from being
        ///     easily accessible in memory.
        /// </remarks>
        /// <param name="arrays">The char arrays containing sensitive information to be cleared.</param>
        /// <exception cref="ArgumentNullException">Thrown if any of the input arrays is null.</exception>
        public static void ClearMemory(char[][] arrays)
        {
            if (arrays == null)
                throw new ArgumentNullException(nameof(arrays), "Input strings cannot be null.");

            foreach (var value in arrays)
            {
                var handle = GCHandle.Alloc(value, GCHandleType.Pinned);

                try
                {
                    Memset.MemSet(handle.AddrOfPinnedObject(), 0, value.Length * sizeof(char));
                }
                catch (AccessViolationException ex)
                {
                    ErrorLogging.ErrorLog(ex);
                    MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    handle.Free();
                }
            }
        }

        public static void ClearMemory(char[] array)
        {
            var handle = GCHandle.Alloc(array, GCHandleType.Pinned);

            try
            {
                Memset.MemSet(handle.AddrOfPinnedObject(), 0, array.Length * sizeof(char));
            }
            catch (AccessViolationException ex)
            {
                ErrorLogging.ErrorLog(ex);
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                handle.Free();
            }
        }

        public static void ClearMemory(char c)
        {
            var handle = GCHandle.Alloc(c, GCHandleType.Pinned);

            try
            {
                Memset.MemSet(handle.AddrOfPinnedObject(), 0, 1);
            }
            catch (AccessViolationException ex)
            {
                ErrorLogging.ErrorLog(ex);
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                handle.Free();
            }
        }

        /// <summary>
        ///     Clears the sensitive information stored in one or more strings securely.
        /// </summary>
        /// <remarks>
        ///     This method uses a pinned string and the SecureMemoryClear function to overwrite the memory
        ///     containing sensitive information, enhancing security by preventing the information from being
        ///     easily accessible in memory.
        /// </remarks>
        /// <param name="str">The strings containing sensitive information to be cleared.</param>
        /// <exception cref="ArgumentNullException">Thrown if any of the input strings is null.</exception>
        public static void ClearMemory(params string[][] str)
        {
            if (str == null)
                throw new ArgumentNullException(nameof(str), "Input strings cannot be null.");

            foreach (var value in str)
            {
                var handle = GCHandle.Alloc(value, GCHandleType.Pinned);

                try
                {
                    Memset.MemSet(handle.AddrOfPinnedObject(), 0, value.Length * 2);
                }
                catch (AccessViolationException ex)
                {
                    ErrorLogging.ErrorLog(ex);
                    MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    handle.Free();
                }
            }
        }

        /// <summary>
        ///     Clears the sensitive information stored in one or more pointers securely.
        /// </summary>
        /// <remarks>
        ///     This method uses a pinned string and the SecureMemoryClear function to overwrite the memory
        ///     containing sensitive information, enhancing security by preventing the information from being
        ///     easily accessible in memory.
        /// </remarks>
        /// <exception cref="ArgumentNullException">Thrown if any of the input strings is null.</exception>
        public static void ClearMemory(int size, ref IntPtr ptr)
        {
            if (ptr == IntPtr.Zero)
                throw new ArgumentNullException(nameof(ptr), "Invalid ptr.");

            var handle = GCHandle.Alloc(ptr, GCHandleType.Pinned);

            try
            {
                Memset.MemSet(handle.AddrOfPinnedObject(), 0, size);
            }
            catch (AccessViolationException ex)
            {
                ErrorLogging.ErrorLog(ex);
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                handle.Free();
            }
        }

        /// <summary>
        ///     Compresses a byte array using the GZip compression algorithm.
        /// </summary>
        /// <remarks>
        ///     This method takes a byte array as input, compresses it using the GZip compression algorithm,
        ///     and returns the compressed byte array. The compression level used is <see cref="CompressionLevel.SmallestSize" />.
        /// </remarks>
        /// <param name="inputText">The byte array representing the uncompressed text to be compressed.</param>
        /// <returns>A compressed byte array using the GZip compression algorithm.</returns>
        /// <exception cref="ArgumentNullException">Thrown if the input byte array is null.</exception>
        public static async Task<byte[]> CompressText(byte[] inputText)
        {
            if (inputText == null)
                throw new ArgumentNullException(nameof(inputText), "Input byte array cannot be null.");

            using var outputStream = new MemoryStream();
            await using (var zipStream = new GZipStream(outputStream, CompressionLevel.SmallestSize, true))
            {
                await zipStream.WriteAsync(inputText).ConfigureAwait(false);
            }

            return outputStream.ToArray();
        }

        /// <summary>
        ///     Decompresses a byte array that was compressed using the GZip compression algorithm.
        /// </summary>
        /// <remarks>
        ///     This method takes a compressed byte array as input, decompresses it using the GZip compression algorithm,
        ///     and returns the decompressed byte array.
        /// </remarks>
        /// <param name="inputText">The byte array representing the compressed text to be decompressed.</param>
        /// <returns>A decompressed byte array using the GZip compression algorithm.</returns>
        /// <exception cref="ArgumentNullException">Thrown if the input byte array is null.</exception>
        public static async Task<byte[]> DecompressText(byte[] inputText)
        {
            if (inputText == null)
                throw new ArgumentNullException(nameof(inputText), "Input byte array cannot be null.");

            using var inputStream = new MemoryStream(inputText);
            await using var zipStream = new GZipStream(inputStream, CompressionMode.Decompress);
            using var outputStream = new MemoryStream();
            await zipStream.CopyToAsync(outputStream);

            return outputStream.ToArray();
        }
    }
/// <summary>
///     Utility class for buffer initialization that contains the necessary keys for cryptographic functions.
/// </summary>
private static class BufferInit
{
    /// <summary>
    ///     Initializes multiple byte arrays from a source byte array for cryptographic operations.
    /// </summary>
    /// <remarks>
    ///     This method takes a source byte array and extracts key components for encryption and HMAC operations.
    ///     It initializes multiple byte arrays for different cryptographic purposes and returns them as a tuple.
    /// </remarks>
    /// <param name="src">The source byte array used for initializing cryptographic buffers.</param>
    /// <returns>
    ///     A tuple containing byte arrays for encryption keys (key, key2, key3, key4, key5)
    ///     and HMAC keys (hMacKey, hMackey2, hMacKey3).
    /// </returns>
    /// <exception cref="ArgumentNullException">Thrown if the source byte array is null.</exception>
    public static (byte[] key, byte[] key2, byte[] key3, byte[] key4, byte[] key5, byte[] hMacKey, byte[]
        hMackey2, byte[] hMacKey3)
        InitBuffers(byte[] src)
    {
        if (src == null)
            throw new ArgumentNullException(nameof(src), "Source byte array cannot be null.");

        var key = new byte[CryptoConstants.KeySize];
        var key2 = new byte[CryptoConstants.ThreeFish];
        var key3 = new byte[CryptoConstants.KeySize];
        var key4 = new byte[CryptoConstants.KeySize];
        var key5 = new byte[CryptoConstants.ShuffleKey];
        var hMacKey = new byte[CryptoConstants.HmacLength];
        var hMackey2 = new byte[CryptoConstants.HmacLength];
        var hMacKey3 = new byte[CryptoConstants.HmacLength];

        CopyBytes(src, key, key2, key3, key4, key5, hMacKey, hMackey2, hMacKey3);

        return (key, key2, key3, key4, key5, hMacKey, hMackey2, hMacKey3);
    }

    /// <summary>
    ///     Copies bytes from a source byte array to multiple destination byte arrays.
    /// </summary>
    /// <remarks>
    ///     This method copies bytes from a source byte array to multiple destination byte arrays.
    ///     It uses Buffer.BlockCopy for efficient copying and advances the offset for each destination array.
    /// </remarks>
    /// <param name="src">The source byte array from which bytes are copied.</param>
    /// <param name="dest">The destination byte arrays where bytes are copied to.</param>
    /// <exception cref="ArgumentNullException">Thrown if the source byte array or any destination byte array is null.</exception>
    /// <exception cref="ArgumentException">
    ///     Thrown if the total length of destination arrays exceeds the length of the source
    ///     array.
    /// </exception>
    private static void CopyBytes(byte[] src, params byte[][] dest)
    {
        if (src == null)
            throw new ArgumentNullException(nameof(src), "Source byte array cannot be null.");

        var currentIndex = 0;

        foreach (var dst in dest)
        {
            if (dst == null)
                throw new ArgumentNullException(nameof(dest), "Destination byte array cannot be null.");

            if (src.Length < dst.Length)
                throw new ArgumentException(
                    "Length of the destination array cannot exceed the length of the source array.");

            Buffer.BlockCopy(src, currentIndex, dst, 0, dst.Length);
            currentIndex += dst.Length;
        }
    }
}
    /// <summary>
    ///     A class that contains different algorithms for encrypting and decrypting.
    /// </summary>
    private static class Algorithms
    {
        /// <summary>
        ///     Generates an array of random indices for shuffling based on a given size and key.
        /// </summary>
        /// <param name="size">The size of the array for which shuffle exchanges are generated.</param>
        /// <param name="key">The key used for generating random indices.</param>
        /// <returns>An array of random indices for shuffling.</returns>
        /// <remarks>
        ///     The method uses a random number generator with the specified key to generate
        ///     unique indices for shuffling a byte array of the given size.
        /// </remarks>
        private static int[] GetShuffleExchanges(int size, byte[] key)
        {
            var exchanges = new int[size - 1];
            var rand = new Random(BitConverter.ToInt32(key));

            for (var i = size - 1; i > 0; i--) exchanges[size - 1 - i] = rand.Next(i + 1);

            return exchanges;
        }

        /// <summary>
        ///     Shuffles a byte array based on a given key using a custom exchange algorithm.
        /// </summary>
        /// <param name="input">The byte array to be shuffled.</param>
        /// <param name="key">The key used for shuffling.</param>
        /// <returns>The shuffled byte array.</returns>
        /// <remarks>
        ///     The shuffling is performed using a custom exchange algorithm based on the specified key.
        /// </remarks>
        public static byte[] Shuffle(byte[] input, byte[] key)
        {
            var size = input.Length;
            var exchanges = GetShuffleExchanges(size, key);

            for (var i = size - 1; i > 0; i--)
            {
                var n = exchanges[size - 1 - i];
                (input[i], input[n]) = (input[n], input[i]);
            }

            return input;
        }

        /// <summary>
        ///     Shuffles a char array based on a given key using a custom exchange algorithm.
        /// </summary>
        /// <param name="input">The char array to be shuffled.</param>
        /// <param name="key">The key used for shuffling.</param>
        /// <returns>The shuffled char array.</returns>
        /// <remarks>
        ///     The shuffling is performed using a custom exchange algorithm based on the specified key.
        /// </remarks>
        public static char[] Shuffle(char[] input, byte[] key)
        {
            var size = input.Length;
            var exchanges = GetShuffleExchanges(size, key);

            for (var i = size - 1; i > 0; i--)
            {
                var n = exchanges[size - 1 - i];
                (input[i], input[n]) = (input[n], input[i]);
            }

            return input;
        }

        /// <summary>
        ///     De-shuffles a byte array based on a given key using a custom exchange algorithm.
        /// </summary>
        /// <param name="input">The byte array to be de-shuffled.</param>
        /// <param name="key">The key used for de-shuffling.</param>
        /// <returns>The de-shuffled byte array.</returns>
        /// <remarks>
        ///     The de-shuffling is performed using a custom exchange algorithm based on the specified key.
        /// </remarks>
        public static byte[] DeShuffle(byte[] input, byte[] key)
        {
            var size = input.Length;
            var exchanges = GetShuffleExchanges(size, key);

            for (var i = 1; i < size; i++)
            {
                var n = exchanges[size - i - 1];
                (input[i], input[n]) = (input[n], input[i]);
            }

            return input;
        }

        /// <summary>
        ///     De-shuffles a char array based on a given key using a custom exchange algorithm.
        /// </summary>
        /// <param name="input">The char array to be de-shuffled.</param>
        /// <param name="key">The key used for de-shuffling.</param>
        /// <returns>The de-shuffled char array.</returns>
        /// <remarks>
        ///     The de-shuffling is performed using a custom exchange algorithm based on the specified key.
        /// </remarks>
        public static char[] DeShuffle(char[] input, byte[] key)
        {
            var size = input.Length;
            var exchanges = GetShuffleExchanges(size, key);

            for (var i = 1; i < size; i++)
            {
                var n = exchanges[size - i - 1];
                (input[i], input[n]) = (input[n], input[i]);
            }

            return input;
        }

        /// <summary>
        ///     Encrypts data using the XChaCha20-Poly1305 authenticated encryption algorithm.
        /// </summary>
        /// <param name="input">The data to be encrypted.</param>
        /// <param name="key">The encryption key.</param>
        /// <param name="nonce">The nonce (number used once) for encryption.</param>
        /// <returns>A byte array representing the encrypted data.</returns>
        public static byte[] EncryptXChaCha20Poly1305(byte[] input, byte[] key, byte[] nonce)
        {
            var result = SecretAeadXChaCha20Poly1305.Encrypt(input, key, nonce);

            return result;
        }

        /// <summary>
        ///     Decrypts data encrypted using the XChaCha20-Poly1305 authenticated encryption algorithm.
        /// </summary>
        /// <param name="input">The encrypted data.</param>
        /// <param name="key">The decryption key.</param>
        /// <param name="nonce">The nonce used during encryption.</param>
        /// <returns>A byte array representing the decrypted data.</returns>
        public static byte[] DecryptXChaCha20Poly1305(byte[] input, byte[] nonce, byte[] key)
        {
            var result = SecretAeadXChaCha20Poly1305.Decrypt(input, nonce, key);

            return result;
        }

        /// <summary>
        ///     Decrypts a byte array that has been encrypted using the AES block cipher in Cipher Block Chaining
        ///     (CBC) mode
        ///     with HMAC-SHA3 authentication.
        /// </summary>
        /// <param name="inputText">The byte array to be decrypted.</param>
        /// <param name="key">The key used for decryption.</param>
        /// <param name="iv"></param>
        /// <param name="hMacKey">The key used for HMAC-SHA3 authentication.</param>
        /// <returns>The decrypted byte array.</returns>
        /// <exception cref="ArgumentException">Thrown when the provided inputText, key, or hMacKey is empty or null.</exception>
        /// <exception cref="CryptographicException">Thrown when the authentication tag does not match.</exception>
        public static byte[] EncryptAes(byte[] inputText, byte[] key, byte[] iv, byte[] hMacKey)
        {
            if (inputText == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(inputText));
            if (key == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(key));
            if (iv == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(iv));
            if (hMacKey == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(hMacKey));

            using var aes = Aes.Create();
            aes.BlockSize = CryptoConstants.BlockBitSize;
            aes.KeySize = CryptoConstants.KeyBitSize;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;

            byte[] cipherText;

            using (var encryptor = aes.CreateEncryptor(key, iv))
            using (var memStream = new MemoryStream())
            {
                using (var cryptoStream = new CryptoStream(memStream, encryptor, CryptoStreamMode.Write))
                using (var cipherStream = new MemoryStream(inputText))
                {
                    cipherStream.CopyTo(cryptoStream, (int)cipherStream.Length);
                    cryptoStream.FlushFinalBlock();
                }

                cipherText = memStream.ToArray();
            }

            var prependItems = new byte[cipherText.Length + iv.Length];
            Buffer.BlockCopy(iv, 0, prependItems, 0, iv.Length);
            Buffer.BlockCopy(cipherText, 0, prependItems, iv.Length, cipherText.Length);

            var tag = HashingMethods.HmacSha3(prependItems, hMacKey);
            var authenticatedBuffer = new byte[prependItems.Length + tag.Length];
            Buffer.BlockCopy(prependItems, 0, authenticatedBuffer, 0, prependItems.Length);
            Buffer.BlockCopy(tag, 0, authenticatedBuffer, prependItems.Length, tag.Length);

            return authenticatedBuffer;
        }

        /// <summary>
        ///     Decrypts a byte array that has been encrypted using the AES block cipher in Cipher Block Chaining
        ///     (CBC) mode
        ///     with HMAC-SHA3 authentication.
        /// </summary>
        /// <param name="inputText">The byte array to be decrypted.</param>
        /// <param name="key">The key used for decryption.</param>
        /// <param name="hMacKey">The key used for HMAC-SHA3 authentication.</param>
        /// <returns>The decrypted byte array.</returns>
        /// <exception cref="ArgumentException">Thrown when the provided inputText, key, or hMacKey is empty or null.</exception>
        /// <exception cref="CryptographicException">Thrown when the authentication tag does not match.</exception>
        public static byte[] DecryptAes(byte[] inputText, byte[] key, byte[] hMacKey)
        {
            if (inputText == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(inputText));
            if (key == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(key));
            if (hMacKey == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(hMacKey));

            using var aes = Aes.Create();
            aes.BlockSize = CryptoConstants.BlockBitSize;
            aes.KeySize = CryptoConstants.KeyBitSize;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;

            var receivedHash = new byte[CryptoConstants.HmacLength];
            Buffer.BlockCopy(inputText, inputText.Length - CryptoConstants.HmacLength, receivedHash, 0,
                CryptoConstants.HmacLength);

            var cipherWithIv = new byte[inputText.Length - CryptoConstants.HmacLength];
            Buffer.BlockCopy(inputText, 0, cipherWithIv, 0, inputText.Length - CryptoConstants.HmacLength);

            var hashedInput = HashingMethods.HmacSha3(cipherWithIv, hMacKey);

            var isMatch = CryptographicOperations.FixedTimeEquals(receivedHash, hashedInput);
            if (!isMatch)
                throw new CryptographicException("Authentication tag does not match.");

            var iv = new byte[CryptoConstants.Iv];
            var cipherResult = new byte[inputText.Length - CryptoConstants.Iv - CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, 0, iv, 0, iv.Length);
            Buffer.BlockCopy(inputText, iv.Length, cipherResult, 0, cipherResult.Length);

            using var decryptor = aes.CreateDecryptor(key, iv);
            using var memStream = new MemoryStream();

            using (var decryptStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Write))
            using (var plainStream = new MemoryStream(cipherResult))
            {
                plainStream.CopyTo(decryptStream, (int)plainStream.Length);
                plainStream.Flush();
                decryptStream.FlushFinalBlock();
            }

            return memStream.ToArray();
        }

        /// <summary>
        ///     Encrypts a byte array using the ThreeFish block cipher in Cipher Block Chaining (CBC) mode with HMAC-SHA3
        ///     authentication.
        /// </summary>
        /// <param name="inputText">The byte array to be encrypted.</param>
        /// <param name="key">The key used for encryption.</param>
        /// <param name="iv">The initialization vector used in CBC mode.</param>
        /// <param name="hMacKey">The key used for HMAC-SHA3 authentication.</param>
        /// <returns>The encrypted and authenticated byte array.</returns>
        public static byte[] EncryptThreeFish(byte[] inputText, byte[] key, byte[] iv, byte[] hMacKey)
        {
            // Initialize ThreeFish block cipher with CBC mode and PKCS7 padding
            var threeFish = new ThreefishEngine(1024);
            var cipherMode = new CbcBlockCipher(threeFish);
            var padding = new Pkcs7Padding();

            var cbcCipher = new PaddedBufferedBlockCipher(cipherMode, padding);

            var keyParam = new KeyParameter(key);
            cbcCipher.Init(true, new ParametersWithIV(keyParam, iv));

            var cipherText = new byte[cbcCipher.GetOutputSize(inputText.Length)];
            var processLength = cbcCipher.ProcessBytes(inputText, 0, inputText.Length, cipherText, 0);
            var finalLength = cbcCipher.DoFinal(cipherText, processLength);
            var finalCipherText = new byte[finalLength + processLength];
            Buffer.BlockCopy(cipherText, 0, finalCipherText, 0, finalCipherText.Length);

            var prependItems = new byte[finalCipherText.Length + iv.Length];
            Buffer.BlockCopy(iv, 0, prependItems, 0, iv.Length);
            Buffer.BlockCopy(finalCipherText, 0, prependItems, iv.Length, finalCipherText.Length);

            var tag = HashingMethods.HmacSha3(prependItems, hMacKey);
            var authenticatedBuffer = new byte[prependItems.Length + tag.Length];
            Buffer.BlockCopy(prependItems, 0, authenticatedBuffer, 0, prependItems.Length);
            Buffer.BlockCopy(tag, 0, authenticatedBuffer, prependItems.Length, tag.Length);

            return authenticatedBuffer;
        }

        /// <summary>
        ///     Decrypts a byte array that has been encrypted using the ThreeFish block cipher in Cipher Block Chaining (CBC) mode
        ///     with HMAC-SHA3 authentication.
        /// </summary>
        /// <param name="inputText">The byte array to be decrypted.</param>
        /// <param name="key">The key used for decryption.</param>
        /// <param name="hMacKey">The key used for HMAC-SHA3 authentication.</param>
        /// <returns>The decrypted byte array.</returns>
        /// <exception cref="ArgumentException">Thrown when the provided inputText, key, or hMacKey is empty or null.</exception>
        /// <exception cref="CryptographicException">Thrown when the authentication tag does not match.</exception>
        public static byte[] DecryptThreeFish(byte[] inputText, byte[] key, byte[] hMacKey)
        {
            if (inputText == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(inputText));
            if (key == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(key));
            if (hMacKey == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(hMacKey));

            var receivedHash = new byte[CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, inputText.Length - CryptoConstants.HmacLength, receivedHash, 0,
                CryptoConstants.HmacLength);

            var cipherWithIv = new byte[inputText.Length - CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, 0, cipherWithIv, 0, inputText.Length - CryptoConstants.HmacLength);

            var hashedInput = HashingMethods.HmacSha3(cipherWithIv, hMacKey);

            var isMatch = CryptographicOperations.FixedTimeEquals(receivedHash, hashedInput);

            if (!isMatch)
                throw new CryptographicException("Authentication tag does not match.");

            var iv = new byte[CryptoConstants.ThreeFish];
            var cipherResult = new byte[inputText.Length - CryptoConstants.ThreeFish - CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, 0, iv, 0, iv.Length);
            Buffer.BlockCopy(inputText, iv.Length, cipherResult, 0, cipherResult.Length);

            var threeFish = new ThreefishEngine(1024);
            var cipherMode = new CbcBlockCipher(threeFish);
            var padding = new Pkcs7Padding();

            var cbcCipher = new PaddedBufferedBlockCipher(cipherMode, padding);

            var keyParam = new KeyParameter(key);
            cbcCipher.Init(false, new ParametersWithIV(keyParam, iv));

            var plainText = new byte[cbcCipher.GetOutputSize(cipherResult.Length)];
            var processLength = cbcCipher.ProcessBytes(cipherResult, 0, cipherResult.Length, plainText, 0);
            var finalLength = cbcCipher.DoFinal(plainText, processLength);
            var finalPlainText = new byte[finalLength + processLength];
            Buffer.BlockCopy(plainText, 0, finalPlainText, 0, finalPlainText.Length);

            return finalPlainText;
        }

        /// <summary>
        ///     Encrypts a byte array using the Serpent block cipher in Cipher Block Chaining (CBC) mode with HMAC-SHA3
        ///     authentication.
        /// </summary>
        /// <param name="inputText">The byte array to be encrypted.</param>
        /// <param name="key">The key used for encryption.</param>
        /// <param name="iv">The initialization vector used in CBC mode.</param>
        /// <param name="hMacKey">The key used for HMAC-SHA3 authentication.</param>
        /// <returns>The encrypted and authenticated byte array.</returns>
        public static byte[] EncryptSerpent(byte[] inputText, byte[] key, byte[] iv, byte[] hMacKey)
        {
            var serpent = new SerpentEngine();
            var cipherMode = new CbcBlockCipher(serpent);
            var padding = new Pkcs7Padding();

            var cbcCipher = new PaddedBufferedBlockCipher(cipherMode, padding);

            var keyParam = new KeyParameter(key);
            cbcCipher.Init(true, new ParametersWithIV(keyParam, iv));

            var cipherText = new byte[cbcCipher.GetOutputSize(inputText.Length)];
            var processLength = cbcCipher.ProcessBytes(inputText, 0, inputText.Length, cipherText, 0);
            var finalLength = cbcCipher.DoFinal(cipherText, processLength);
            var finalCipherText = new byte[finalLength + processLength];
            Buffer.BlockCopy(cipherText, 0, finalCipherText, 0, finalCipherText.Length);

            var prependItems = new byte[finalCipherText.Length + iv.Length];
            Buffer.BlockCopy(iv, 0, prependItems, 0, iv.Length);
            Buffer.BlockCopy(finalCipherText, 0, prependItems, iv.Length, finalCipherText.Length);

            var tag = HashingMethods.HmacSha3(prependItems, hMacKey);
            var authenticatedBuffer = new byte[prependItems.Length + tag.Length];
            Buffer.BlockCopy(prependItems, 0, authenticatedBuffer, 0, prependItems.Length);
            Buffer.BlockCopy(tag, 0, authenticatedBuffer, prependItems.Length, tag.Length);

            return authenticatedBuffer;
        }

        /// <summary>
        ///     Decrypts a byte array that has been encrypted using the Serpent block cipher in Cipher Block Chaining (CBC) mode
        ///     with HMAC-SHA3 authentication.
        /// </summary>
        /// <param name="inputText">The byte array to be decrypted.</param>
        /// <param name="key">The key used for decryption.</param>
        /// <param name="hMacKey">The key used for HMAC-SHA3 authentication.</param>
        /// <returns>The decrypted byte array.</returns>
        /// <exception cref="ArgumentException">Thrown when the provided inputText, key, or hMacKey is empty or null.</exception>
        /// <exception cref="CryptographicException">Thrown when the authentication tag does not match.</exception>
        public static byte[] DecryptSerpent(byte[] inputText, byte[] key, byte[] hMacKey)
        {
            if (inputText == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(inputText));
            if (key == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(key));
            if (hMacKey == Array.Empty<byte>())
                throw new ArgumentException("Value was empty or null.", nameof(hMacKey));

            var receivedHash = new byte[CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, inputText.Length - CryptoConstants.HmacLength, receivedHash, 0,
                CryptoConstants.HmacLength);

            var cipherWithIv = new byte[inputText.Length - CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, 0, cipherWithIv, 0, inputText.Length - CryptoConstants.HmacLength);

            var hashedInput = HashingMethods.HmacSha3(cipherWithIv, hMacKey);

            var isMatch = CryptographicOperations.FixedTimeEquals(receivedHash, hashedInput);

            if (!isMatch)
                throw new CryptographicException("Authentication tag does not match.");

            var iv = new byte[CryptoConstants.Iv];
            var cipherResult = new byte[inputText.Length - CryptoConstants.Iv - CryptoConstants.HmacLength];

            Buffer.BlockCopy(inputText, 0, iv, 0, iv.Length);
            Buffer.BlockCopy(inputText, iv.Length, cipherResult, 0, cipherResult.Length);

            var serpent = new SerpentEngine();
            var cipherMode = new CbcBlockCipher(serpent);
            var padding = new Pkcs7Padding();

            var cbcCipher = new PaddedBufferedBlockCipher(cipherMode, padding);

            var keyParam = new KeyParameter(key);
            cbcCipher.Init(false, new ParametersWithIV(keyParam, iv));

            var plainText = new byte[cbcCipher.GetOutputSize(cipherResult.Length)];
            var processLength = cbcCipher.ProcessBytes(cipherResult, 0, cipherResult.Length, plainText, 0);
            var finalLength = cbcCipher.DoFinal(plainText, processLength);
            var finalPlainText = new byte[finalLength + processLength];
            Buffer.BlockCopy(plainText, 0, finalPlainText, 0, finalPlainText.Length);

            return finalPlainText;
        }
    }
  /// <summary>
  ///     A class that contains encryption and decryption methods.
  /// </summary>
  private static class EncryptionDecryption
  {
      /// <summary>
      ///     Asynchronously encrypts a byte array using a multi-layer encryption approach.
      /// </summary>
      /// <param name="plaintext">The byte array to be encrypted.</param>
      /// <param name="key">The key used for the first layer of encryption (XChaCha20-Poly1305).</param>
      /// <param name="key2">The key used for the second layer of encryption (ThreeFish).</param>
      /// <param name="key3">The key used for the third layer of encryption.</param>
      /// <param name="key4">The key used for the fourth layer of encryption.</param>
      /// <param name="key5">The key used for shuffling the final encrypted result.</param>
      /// <param name="hMacKey">The key used for HMAC in the second layer of encryption.</param>
      /// <param name="hMacKey2">The key used for HMAC in the third layer of encryption.</param>
      /// <param name="hMacKey3">The key used for HMAC in the fourth layer of encryption.</param>
      /// <returns>A shuffled byte array containing nonces and the final encrypted result.</returns>
      /// <exception cref="ArgumentException">Thrown when any of the input parameters is an empty array.</exception>
      /// <exception cref="Exception">Thrown when any intermediate or final encrypted value is empty.</exception>
      public static byte[] EncryptV3(ref byte[] plaintext,
          ref byte[] key, ref byte[] key2, ref byte[] key3, ref byte[] key4, ref byte[] key5, ref byte[] hMacKey,
          ref byte[] hMacKey2,
          ref byte[] hMacKey3)
      {
          if (plaintext == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(plaintext));
          if (key == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key));
          if (key2 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key2));
          if (key3 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key3));
          if (key4 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key4));
          if (key5 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key5));
          if (hMacKey == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(hMacKey));
          if (hMacKey2 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(hMacKey2));
          if (hMacKey3 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(hMacKey3));

          var nonce = CryptoUtilities.RndByteSized(CryptoConstants.ChaChaNonceSize);
          var nonce2 = CryptoUtilities.RndByteSized(CryptoConstants.ThreeFish);
          var nonce3 = CryptoUtilities.RndByteSized(CryptoConstants.Iv);
          var nonce4 = CryptoUtilities.RndByteSized(CryptoConstants.Iv);

          var cipherText = Algorithms.EncryptXChaCha20Poly1305(plaintext, nonce, key) ??
                           throw new Exception("Value was empty.");
          var cipherTextL2 = Algorithms.EncryptThreeFish(cipherText, key2, nonce2, hMacKey) ??
                             throw new Exception("Value was empty.");
          var cipherTextL3 = Algorithms.EncryptSerpent(cipherTextL2, key3, nonce3, hMacKey2) ??
                             throw new Exception("Value was empty.");
          var cipherTextL4 = Algorithms.EncryptAes(cipherTextL3, key4, nonce4, hMacKey3) ??
                             throw new Exception("Value was empty.");

          var result = nonce.Concat(nonce2).Concat(nonce3).Concat(nonce4).Concat(cipherTextL4).ToArray();
          var shuffledResult = Algorithms.Shuffle(result, key5);

          return shuffledResult;
      }

      /// <summary>
      ///     Asynchronously decrypts a byte array that has been encrypted using a multi-layer encryption approach.
      /// </summary>
      /// <param name="cipherText">The byte array to be decrypted.</param>
      /// <param name="key">The key used for the first layer of decryption (XChaCha20-Poly1305).</param>
      /// <param name="key2">The key used for the second layer of decryption (ThreeFish).</param>
      /// <param name="key3">The key used for the third layer of decryption.</param>
      /// <param name="key4">The key used for the fourth layer of encryption.</param>
      /// <param name="key5">The key used for unshuffling the ciphertext.</param>
      /// <param name="hMacKey">The key used for HMAC in the second layer of decryption.</param>
      /// <param name="hMacKey2">The key used for HMAC in the third layer of decryption.</param>
      /// <param name="hMacKey3">The key used for HMAC in the fourth layer of encryption.</param>
      /// <returns>The decrypted byte array.</returns>
      /// <exception cref="ArgumentException">Thrown when any of the input parameters is an empty array.</exception>
      /// <exception cref="Exception">Thrown when any intermediate or final decrypted value is empty.</exception>
      public static byte[] DecryptV3(ref byte[] cipherText,
          ref byte[] key, ref byte[] key2, ref byte[] key3, ref byte[] key4, ref byte[] key5, ref byte[] hMacKey,
          ref byte[] hMacKey2,
          ref byte[] hMacKey3)
      {
          if (cipherText == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(cipherText));
          if (key == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key));
          if (key2 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key2));
          if (key3 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key3));
          if (key4 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key4));
          if (key5 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(key5));
          if (hMacKey == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(hMacKey));
          if (hMacKey2 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(hMacKey2));
          if (hMacKey3 == Array.Empty<byte>())
              throw new ArgumentException("Value was empty.", nameof(hMacKey3));

          var unshuffledResult = Algorithms.DeShuffle(cipherText, key5);

          var nonce = new byte[CryptoConstants.ChaChaNonceSize];
          Buffer.BlockCopy(unshuffledResult, 0, nonce, 0, nonce.Length);
          var nonce2 = new byte[CryptoConstants.ThreeFish];
          Buffer.BlockCopy(unshuffledResult, nonce.Length, nonce2, 0, nonce2.Length);
          var nonce3 = new byte[CryptoConstants.Iv];
          Buffer.BlockCopy(unshuffledResult, nonce.Length + nonce2.Length, nonce3, 0, nonce3.Length);
          var nonce4 = new byte[CryptoConstants.Iv];
          Buffer.BlockCopy(unshuffledResult, nonce.Length + nonce2.Length + nonce3.Length, nonce4, 0, nonce4.Length);

          var cipherResult =
              new byte[unshuffledResult.Length - nonce4.Length - nonce3.Length - nonce2.Length - nonce.Length];

          Buffer.BlockCopy(unshuffledResult, nonce.Length + nonce2.Length + nonce3.Length + nonce4.Length,
              cipherResult,
              0,
              cipherResult.Length);

          var resultL4 = Algorithms.DecryptAes(cipherResult, key4, hMacKey3) ??
                         throw new Exception("Value was empty.");
          var resultL3 = Algorithms.DecryptSerpent(resultL4, key3, hMacKey2) ??
                         throw new Exception("Value was empty.");
          var resultL2 = Algorithms.DecryptThreeFish(resultL3, key2, hMacKey) ??
                         throw new Exception("Value was empty.");
          var result = Algorithms.DecryptXChaCha20Poly1305(resultL2, nonce, key) ??
                       throw new Exception("Value was empty.");

          return result;
      }
  }

I have most of it documented. I realize this is overkill, but I'd like anyone with knowledge of cryptography to chime in here.

\$\endgroup\$
6
  • \$\begingroup\$ The easy answer is "don't" to all of this. It's notoriously difficult to write provably secure cryptosystems, and any serious security professional would have to assume that this is insecure because it's home-rolled and non-vetted. \$\endgroup\$ Commented Dec 13, 2024 at 16:23
  • 1
    \$\begingroup\$ @Reinderien How does one become a serious security professional without asking questions? \$\endgroup\$ Commented Dec 14, 2024 at 13:37
  • 1
    \$\begingroup\$ That's certainly a good point, and so in context, it's good to learn on a question like this, but also, the OP describing that this is used for a password vault application suggests that it's being used in production which it shouldn't. \$\endgroup\$ Commented Dec 14, 2024 at 13:39
  • \$\begingroup\$ Perhaps more specifically, this site is good at covering generic software design, but cryptographic robustness is better covered on crypto.stackexchange.com . \$\endgroup\$ Commented Dec 14, 2024 at 17:20
  • 2
    \$\begingroup\$ @Reinderien the cryptography doesn't seem to be the core of the question here — this is just a composition of four well known ciphers (there may well be bugs somewhere else which could introduce vulnerabilities, but the cryptographic primitives themselves aren't home-rolled). I think the main question here is software design \$\endgroup\$ Commented Dec 14, 2024 at 19:38

1 Answer 1

7
\$\begingroup\$

The code is not mature and should not be used in production. It doesn't adhere to cryptography best practices, even though the code is clearly written with knowledge of quite a few of these principles.

Cryptographers generally have high confidence in most cipher algorithms such as AES, so this class doesn't seem needed. Using password based encryption is much more of a concern. Using hybrid encryption with a classic and PQC cipher is a much better idea, as you don't even need the private key during encryption.

Finally, the library is very memory inefficient.

General issues

The protocol isn't specified. It is unclear to the reader of the documentation how the encryption takes place, which algorithms are used and when etc.


This assumes that the file is filled with text, but this is not mentioned anywhere. Usually files are encrypted as binary.


It doesn't make much sense to load the entire file into a string in memory, then to convert it to bytes in memory, and then to store the ciphertext in memory too. This thing will create new arrays for each ciphertext, and then also forgets to wipe the intermediate arrays. The keys are also not directly destroyed before the next run.


The plaintext bytes are wiped, but as you just loaded the file into a string, the plaintext will still be in memory after encryption, until it gets garbage collected - if ever.


EncryptFile and DecryptFile do not encrypt the file that is passed in, instead they return the ciphertext and plaintext respectively.


public static int BoundedInt(int min, int max)

This will generally return a biased number as lower values in the range will be more prominent.


The entire shuffle idea doesn't add security, only complexity. Key derivation already produces random results. At the worst, an attacker can use the shuffle method to wait for a configuration that is beneficial to attack.


Container formats should have a version. Configuration options such as iteration count should be stored with the ciphertext, or they should be linked to a specific version. Salts should be stored with each file; I'm hoping here that the salt differs for each file or the same keyset will be used for each file encryption.


Compression is a well known side channel. Leaking the size of the plaintext can also be a side channel of course and can give a lot of info on what is stored, but adding compression generally makes this worse as the resulting size is now also content-specific.

Minor notes

username and password are single words, so no need to use camelCase for them.


if (fileBytes == null || fileBytes.Length == 0 || salt == null || salt.Length == 0)
    throw new ArgumentException("Value was empty.");

This doesn't show what value was empty. And empty files are just "normal" files. There is no specific need to treat them differently.


Constants are not all UPPERCASE. Hash and SecurePassword (this time Password is a full word I guess) etc. are not constants.


It doesn't make much sense to store the RndNum in a static field, creating these kind of objects is generally lightweight.


It doesn't make much sense to group all constants, hash methods etc. together. Code should be grouped by functionality, and the constants should be included with the code that does encryption or message authentication. The password hashing can also be replaced by e.g. public key encryption so it should be in a separate class.


Encryption using a cipher uses the generic Engine construct is always following the same idea. It is possible to simply create a single interface for encryption. If you need to use the Cipher construct then just create a small adapter so that you can use the same interface.

The good things

You're using Aes.Create() rather than the constructor, which will use an optimized AES when used.


Cipher's implementations are only instantiated when needed. Cipher and engine instances are relatively lightweight, so this makes more sense than having them as fields.


In principle encrypting with multiple algorithms can add some security in case any of the algorithms is broken. It's a bit overkill and of course it is much more likely that e.g. a bad password kills security, but in principle it does work.

Note that you're not quadrupling key size as meet-in-the-middle attacks are theoretically possible. This is not a problem, you don't need that large a key size anyway and the memory requirements would be infeasible. So this is OK as long as long as you don't put down bad claims.


The keys are derived using a modern (PB)KDF. Furthermore, HMAC is used for authentication, and that's a well proven (if relatively slow) method of authenticating ciphertext.


Ciphertext is authenticated rather than the plaintext, encrypt-then-MAC is the way to go. The IV is indeed included within the HMAC calculation.


Code is relatively easy to read and most identifiers are reasonable. Guard statements are also provided, maybe even overly so (if some methods are made private then the checking would not be necessary, and most people can do without a wrapper for EncryptThreeFish fine).

Hints

If you have a set of keys then create a data class around it. That way you can be sure that the state is captured correctly and all this exception handling is not required anymore. This can also have a clear method etc. It also allows separate unit testing of the class. And it saves a crapload of typing out documentation when done correctly.


One of the hints of the .NET cryptography API is that they are using streams. That should have been a hint on how to best perform encryption / decryption. For instance, it would be an idea to "stack streams" and perform encryption / decryption directly. You can always put the result in a MemoryArray or write it to a temporary file which can be discarded if the authentication tag calculation fail.

If you do want to store things in memory, then the input and output array can usually be the same (as long as the input isn't overwritten before it is read) for ciphers. It is possible to rewrite this using a single array used for both the plaintext and the intermediate ciphertexts.


Try and work without global variables. You can create a class that simply gets a configuration without static variables fine, so instances can be configured separately. This could be helpful when settings change in time.


The part where the ciphertext is written to file seems to be missing. However, I'd warn you that most filesystems do not allow atomic actions on files. If the file writing part is interrupted then you might end up with a file that contains partial bytes. The authentication of these files will fail, and you will have no way of knowing why. Storing a size / checksum at the start of the file may alleviate that issue.

\$\endgroup\$
0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.