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.
Thanks for any information!