1
$\begingroup$

I'm looking for maximum possible security. My current scheme is xChaCha20-Poly1305-xSalsa20-Poly1305-AES-CBC-HMACSHA-512. Does combining these algorithms increase security if implemented correctly? Here's my source code for reference:

    private const int Iterations = 32;
    private const double MemorySize = 1024d * 1024d * 5d;
    public const int SaltSize = 512 / 8;
    private const int HmacLength = 64;
    private const int ChaChaNonceSize = 24;
    public static readonly int KeySize = 32;
    public static readonly int IvBit = 128;

    private static readonly RandomNumberGenerator RndNum = RandomNumberGenerator.Create();
    public static byte[] RndByteSized(int size)
    {
        var buffer = new byte[size];
        RndNum.GetBytes(buffer);
        return buffer;
    }

    private static (byte[] cipherResult, byte[] iv) InitBuffer(byte[] cipherText)
    {
        var iv = new byte[IvBit / 8];
        var cipherResult = new byte[cipherText.Length - iv.Length];

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

        return (cipherResult, iv);
    }
    private const int BlockBitSize = 128;
    private const int KeyBitSize = 256;

    public static async Task<byte[]> EncryptAsync(byte[] plainText, byte[] key, byte[] iv, byte[] salt)
    {
        try
        {
            if (plainText == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(plainText));
            if (key == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(key));
            if (salt == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(salt));
            if (iv == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(iv));

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

            using var argon2 = new Argon2id(key);
            argon2.Salt = salt;
            argon2.DegreeOfParallelism = Environment.ProcessorCount * 2;
            argon2.Iterations = Iterations;
            argon2.MemorySize = (int)MemorySize;

            var hmacKey = await argon2.GetBytesAsync(HmacLength);

            byte[] cipherText;

            using (var encryptor = aes.CreateEncryptor(key, iv))
            using (var memStream = new MemoryStream())
            {
                await using (var cryptoStream =
                             new CryptoStream(memStream, encryptor, CryptoStreamMode.Write))
                {
                    using (var cipherStream = new MemoryStream(plainText))
                    {
                        cipherStream.FlushAsync();
                        cipherStream.CopyToAsync(cryptoStream, (int)cipherStream.Length);
                    }

                    cryptoStream.FlushFinalBlockAsync();
                }

                cipherText = memStream.ToArray();
            }

            Array.Clear(key, 0, key.Length);

            using var hmac = new HMACSHA512(hmacKey);
            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 = hmac.ComputeHash(prependItems);
            var authenticatedBuffer = prependItems.Length + tag.Length;
            var authenticatedBytes = new byte[authenticatedBuffer];

            Buffer.BlockCopy(prependItems, 0, authenticatedBytes, 0, prependItems.Length);
            Buffer.BlockCopy(tag, 0, authenticatedBytes, prependItems.Length, tag.Length);
            Array.Clear(hmacKey, 0, hmacKey.Length);

            return authenticatedBytes;
        }
        catch (CryptographicException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (ArgumentNullException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (ObjectDisposedException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (Exception ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
    }

    public static async Task<byte[]> DecryptAsync(byte[] cipherText, byte[] key, byte[] salt)
    {
        try
        {
            if (cipherText == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(cipherText));
            if (key == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(key));
            if (salt == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty or null.", nameof(salt));

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

            using var argon2 = new Argon2id(key);
            argon2.Salt = salt;
            argon2.DegreeOfParallelism = Environment.ProcessorCount * 2;
            argon2.Iterations = Iterations;
            argon2.MemorySize = (int)MemorySize;

            var hmacKey = await argon2.GetBytesAsync(HmacLength);

            using var hmac = new HMACSHA512(hmacKey);
            var receivedHash = new byte[HmacLength];

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

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

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

            var hashedInput = hmac.ComputeHash(cipherWithIv);

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

            if (!isMatch)
                throw new CryptographicException("Invalid tag.");

            var (cipherResult, iv) = InitBuffer(cipherWithIv);

            using var decryptor = aes.CreateDecryptor(key, iv);
            using var memStream = new MemoryStream();
            await using (var decryptStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Write))
            {
                using (var plainStream = new MemoryStream(cipherResult))
                {
                    plainStream.CopyTo(decryptStream, (int)plainStream.Length);
                    plainStream.FlushAsync();
                }

                await decryptStream.FlushFinalBlockAsync();
            }

            Array.Clear(key, 0, key.Length);

            return memStream.ToArray();
        }
        catch (CryptographicException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (ArgumentNullException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (ObjectDisposedException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (Exception ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
#pragma warning restore
    }

    public static byte[] EncryptXSalsaPoly1305(byte[] input, byte[] key, byte[] nonce)
    {
        var result = StreamEncryption.Encrypt(input, nonce, key);
        var authentication = OneTimeAuth.Sign(result, key);
        return result.Concat(authentication).ToArray();
    }

    public static byte[] DecryptXSalsaPoly1305(byte[] input, byte[] key, byte[] nonce)
    {
        try
        {
            var receivedHash = new byte[16];
            Buffer.BlockCopy(input, input.Length - receivedHash.Length, receivedHash, 0, receivedHash.Length);
            var inputText = new byte[input.Length - receivedHash.Length];
            Buffer.BlockCopy(input, 0, inputText, 0, inputText.Length);

            var originalHash = OneTimeAuth.Sign(inputText, key);
            var isMatch = CryptographicOperations.FixedTimeEquals(receivedHash, originalHash);

            if (!isMatch)
                throw new CryptographicException("Invalid tag.");

            var result = StreamEncryption.Decrypt(inputText, nonce, key);

            return result;
        }
        catch (CryptographicException ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (Exception ex)
        {
            Array.Clear(key, 0, key.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
    }

    public static async Task<byte[]> EncryptAsyncV3(byte[] plaintext, byte[] salt, byte[] salt2, byte[] salt3,
        byte[] password)
    {
        using var argon2 = new Argon2id(password);
        argon2.Salt = salt;
        argon2.DegreeOfParallelism = Environment.ProcessorCount * 2;
        argon2.Iterations = Iterations;
        argon2.MemorySize = (int)MemorySize;

        var key = await argon2.GetBytesAsync(KeySize);

        using var argon2L2 = new Argon2id(key);
        argon2L2.Salt = salt2;
        argon2L2.DegreeOfParallelism = Environment.ProcessorCount * 2;
        argon2L2.Iterations = Iterations;
        argon2L2.MemorySize = (int)MemorySize;

        var key2 = await argon2L2.GetBytesAsync(KeySize);

        using var argon2L3 = new Argon2id(key2);
        argon2L3.Salt = salt3;
        argon2L3.DegreeOfParallelism = Environment.ProcessorCount * 2;
        argon2L3.Iterations = Iterations;
        argon2L3.MemorySize = (int)MemorySize;

        var key3 = await argon2L3.GetBytesAsync(KeySize);

        try
        {
            var nonce = RndByteSized(ChaChaNonceSize);
            var nonce2 = RndByteSized(ChaChaNonceSize);
            var nonce3 = RndByteSized(IvBit / 8);

            var cipherText = SecretAeadXChaCha20Poly1305.Encrypt(plaintext, nonce, key);
            var cipherTextL2 = EncryptXSalsaPoly1305(cipherText, key2, nonce2);
            var cipherTextL3 = await EncryptAsync(cipherTextL2, key3, nonce3, salt);

            Array.Clear(key, 0, key.Length);
            Array.Clear(key2, 0, key2.Length);
            Array.Clear(key3, 0, key3.Length);

            return nonce.Concat(nonce2).Concat(nonce3).Concat(cipherTextL3).ToArray();
        }
        catch (CryptographicException ex)
        {
            Array.Clear(key, 0, key.Length);
            Array.Clear(key2, 0, key2.Length);
            Array.Clear(key3, 0, key3.Length);
            Array.Clear(password, 0, password.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (Exception ex)
        {
            Array.Clear(key, 0, key.Length);
            Array.Clear(key2, 0, key2.Length);
            Array.Clear(key3, 0, key3.Length);
            Array.Clear(password, 0, password.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
    }

    public static async Task<byte[]> DecryptAsyncV3(byte[] cipherText, byte[] salt, byte[] salt2, byte[] salt3,
        byte[] password)
    {
        using var argon2 = new Argon2id(password);
        argon2.Salt = salt;
        argon2.DegreeOfParallelism = Environment.ProcessorCount * 2;
        argon2.Iterations = Iterations;
        argon2.MemorySize = (int)MemorySize;

        var key = await argon2.GetBytesAsync(KeySize);

        using var argon2L2 = new Argon2id(key);
        argon2L2.Salt = salt2;
        argon2L2.DegreeOfParallelism = Environment.ProcessorCount * 2;
        argon2L2.Iterations = Iterations;
        argon2L2.MemorySize = (int)MemorySize;

        var key2 = await argon2L2.GetBytesAsync(KeySize);

        using var argon2L3 = new Argon2id(key2);
        argon2L3.Salt = salt3;
        argon2L3.DegreeOfParallelism = Environment.ProcessorCount * 2;
        argon2L3.Iterations = Iterations;
        argon2L3.MemorySize = (int)MemorySize;

        var key3 = await argon2L3.GetBytesAsync(KeySize);

        try
        {
            if (cipherText == Array.Empty<byte>() || salt == Array.Empty<byte>() || salt2 == Array.Empty<byte>()
                || salt3 == Array.Empty<byte>() || password == Array.Empty<byte>())
                throw new ArgumentException(@"Value was empty.",
                    cipherText == Array.Empty<byte>() ? nameof(cipherText) :
                    salt == Array.Empty<byte>() ? nameof(salt) :
                    salt2 == Array.Empty<byte>() ? nameof(salt2) :
                    salt3 == Array.Empty<byte>() ? nameof(salt3) :
                    nameof(password));

            var nonce = new byte[ChaChaNonceSize];
            Buffer.BlockCopy(cipherText, 0, nonce, 0, nonce.Length);

            var nonce2 = new byte[ChaChaNonceSize];
            Buffer.BlockCopy(cipherText, nonce.Length, nonce2, 0, nonce2.Length);

            var nonce3 = new byte[IvBit / 8];
            Buffer.BlockCopy(cipherText, nonce.Length + nonce2.Length, nonce3, 0, nonce3.Length);

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

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

            var resultL3 = await DecryptAsync(cipherResult, key3, salt);
            var resultL2 = DecryptXSalsaPoly1305(resultL3, key2, nonce2);
            var resultL0 = SecretAeadXChaCha20Poly1305.Decrypt(resultL2, nonce, key);


            Array.Clear(key, 0, key.Length);
            Array.Clear(key2, 0, key2.Length);
            Array.Clear(key3, 0, key3.Length);

            return resultL0;
        }
        catch (CryptographicException ex)
        {
            Array.Clear(key, 0, key.Length);
            Array.Clear(key2, 0, key2.Length);
            Array.Clear(key3, 0, key3.Length);
            Array.Clear(password, 0, password.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
        catch (Exception ex)
        {
            Array.Clear(key, 0, key.Length);
            Array.Clear(key2, 0, key2.Length);
            Array.Clear(key3, 0, key3.Length);
            Array.Clear(password, 0, password.Length);
            ErrorLogging.ErrorLog(ex);
            return Array.Empty<byte>();
        }
    }

Everything seems to encrypt / decrypt fine, just wanting to make sure this is all implemented correctly and if this scheme adds security versus having one layer of encryption.

$\endgroup$
2
  • 1
    $\begingroup$ Unless you are sure that one of these technologies is likely to be broken, or that one of your keys will be leaked, this is most likely pseudo-security. Its not necessarily less secure (although poor implementation could mean it is), but you see the problem yourself: Your implementation gets a lot more complicated. If AES256 is enough for the US Governments TOP SECRET, it is good enough for you. $\endgroup$
    – tistorm
    Commented Nov 14, 2023 at 17:55
  • 1
    $\begingroup$ isiweb.ee.ethz.ch/archive/massey_pub/pdf/BI434.pdf is worth reading. The security of a cascade of ciphers in general is limited to the security of the first cipher used, though if all of them are additive stream ciphers it's instead the security of the strongest. But it's not higher than any of the component ciphers, so it's usually pointless. $\endgroup$ Commented Nov 15, 2023 at 3:53

1 Answer 1

0
$\begingroup$

Combining AEAD's like this doesn't really make sense. Also chaining Salsa and ChaCha is useless, ChaCha is an improved Salsa.

You would be better served by just increasing the number of rounds of the primitives. For ChaCha it's very straightforward and requires just changing a single number.

For example, xChaCha60-Poly1305 will likely never be broken as long as P does not equal NP. Otherwise all crypto is doomed because the boolean satisfiability problem will be solvable.

It only makes sense to do this if you are paranoid and don't care about losing significant performance.

You can also do the same with AES, but then might as well replace the AES key schedule with SHAKE128 or something.

$\endgroup$
2
  • $\begingroup$ how could i go about changing the amount of rounds with chacha? libsodium is a locked reference in c# $\endgroup$ Commented Nov 15, 2023 at 0:22
  • $\begingroup$ I know nothing about c sharp, the change would have to be implemented where the ChaCha permutation resides. $\endgroup$ Commented Nov 15, 2023 at 0:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.