I’m using two ciphers in this code that are in this order- encrypt plain text with xchacha20-poly1305 then encrypt the resulting ciphertext with aes-cbc-blake2b hmac.
I was told elsewhere rather than derive 2 keys from one key that’s derived from a password, it would be better to derive all the bytes from the password and split them into 3 keys.
I was also wondering if having two ciphers with different keys adds a layer of security or not.
Is this a safe and secure implementation of aes-cbc-hmac combined with xchacha20-poly1305?
Code:
private const int Iterations = 32; // 32 iterations in release build
private const double MemorySize = 1024d * 1024d * 5d; // 5gib in release build
public const int SaltSize = 512 / 8;
public static readonly int KeySize = 32;
public static readonly int IvBit = 128;
private const int HmacLength = 64;
private const int ChaChaNonceSize = 24;
private static readonly RandomNumberGenerator RndNum = RandomNumberGenerator.Create();
public static async Task<(byte[] Salt, byte[] Salt2, byte[] Salt3, byte[] Salt4)> GetUserSaltAsync(string userName)
{
try
{
var salt = await File.ReadAllTextAsync(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Password Vault", "Users", userName, $"{userName}.salt"));
var salt2 = await File.ReadAllTextAsync(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Password Vault", "Users", userName, $"{userName}-Salt2.salt"));
var salt3 = await File.ReadAllTextAsync(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Password Vault", "Users", userName, $"{userName}-Salt3.salt"));
var salt4 = await File.ReadAllTextAsync(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Password Vault", "Users", userName, $"{userName}-Salt4.salt"));
var saltResult = DataConversionHelpers.Base64StringToByteArray(salt);
var saltResult2 = DataConversionHelpers.Base64StringToByteArray(salt2);
var saltResult3 = DataConversionHelpers.Base64StringToByteArray(salt3);
var saltResult4 = DataConversionHelpers.Base64StringToByteArray(salt4);
return (saltResult, saltResult2, saltResult3, saltResult4);
}
catch (IOException ex)
{
MessageBox.Show(ex.Message, @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
ErrorLogging.ErrorLog(ex);
return (Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>());
}
}
public static byte[] RndByteSized(int size)
{
var buffer = new byte[size];
RndNum.GetBytes(buffer);
return buffer;
}
public static async Task<byte[]> EncryptFile(string userName, char[] passWord, string file)
{
try
{
if (userName == string.Empty || passWord == Array.Empty<char>() || file == string.Empty)
throw new ArgumentException(@"Value was empty.", userName == string.Empty ? nameof(userName) :
passWord == Array.Empty<char>() ? nameof(passWord) : nameof(file));
var saltResult = await Authentication.GetUserSaltAsync(userName);
var salt = saltResult.Salt;
var salt2 = saltResult.Salt2;
var salt3 = saltResult.Salt3;
var fileStr = await File.ReadAllTextAsync(file);
var fileBytes = DataConversionHelpers.StringToByteArray(fileStr);
var passwordBytes = Encoding.UTF8.GetBytes(passWord);
if (fileBytes == Array.Empty<byte>() || salt == Array.Empty<byte>())
throw new ArgumentException(@"Value was empty.",
fileBytes == Array.Empty<byte>() ? nameof(fileBytes) : nameof(salt));
var encryptedFile = await EncryptAsyncV3(fileBytes, salt, salt2, salt3, passwordBytes);
Array.Clear(passWord, 0, passWord.Length);
return encryptedFile;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
}
public static async Task<byte[]> DecryptFile(string userName, char[] passWord, string file)
{
try
{
if (userName == string.Empty || passWord == Array.Empty<char>() || file == string.Empty)
throw new ArgumentException(@"Value was empty.", userName == string.Empty ? nameof(userName) :
passWord == Array.Empty<char>() ? nameof(passWord) : nameof(file));
var saltResult = await Authentication.GetUserSaltAsync(userName);
var salt = saltResult.Salt;
var salt2 = saltResult.Salt2;
var salt3 = saltResult.Salt3;
var fileStr = await File.ReadAllTextAsync(file);
var fileBytes = DataConversionHelpers.Base64StringToByteArray(fileStr);
var passwordBytes = Encoding.UTF8.GetBytes(passWord);
if (fileBytes == Array.Empty<byte>() || salt == Array.Empty<byte>())
throw new ArgumentException(@"Value was empty.",
fileBytes == Array.Empty<byte>() ? nameof(fileBytes) : nameof(salt));
var decryptedFile = await DecryptAsyncV3(fileBytes, salt, salt2, salt3, passwordBytes);
Array.Clear(passWord, 0, passWord.Length);
return decryptedFile;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
}
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)
{
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));
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);
try
{
using var aes = Aes.Create();
aes.BlockSize = BlockBitSize;
aes.KeySize = KeyBitSize;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
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))
{
await cipherStream.FlushAsync();
await cipherStream.CopyToAsync(cryptoStream, (int)cipherStream.Length);
}
await cryptoStream.FlushFinalBlockAsync();
}
cipherText = memStream.ToArray();
}
Array.Clear(key, 0, key.Length);
using var hmac = new HMACBlake2B(hmacKey, HmacLength * 8);
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);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
catch (ArgumentNullException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
catch (ObjectDisposedException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
catch (Exception ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
}
public static async Task<byte[]> DecryptAsync(byte[] cipherText, byte[] key, byte[] salt)
{
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 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);
try
{
using var aes = Aes.Create();
aes.BlockSize = BlockBitSize;
aes.KeySize = KeyBitSize;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var hmac = new HMACBlake2B(hmacKey, HmacLength * 8);
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();
Array.Clear(hmacKey, 0, hmacKey.Length);
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))
{
await plainStream.CopyToAsync(decryptStream, (int)plainStream.Length);
await plainStream.FlushAsync();
}
await decryptStream.FlushFinalBlockAsync();
}
Array.Clear(key, 0, key.Length);
return memStream.ToArray();
}
catch (CryptographicException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
catch (ArgumentNullException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
catch (ObjectDisposedException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
catch (Exception ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(hmacKey, 0, hmacKey.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
#pragma warning restore
}
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);
try
{
if (plaintext == Array.Empty<byte>() || salt == Array.Empty<byte>() || salt2 == Array.Empty<byte>()
|| password == Array.Empty<byte>())
throw new ArgumentException(@"Value was empty.",
plaintext == Array.Empty<byte>() ? nameof(plaintext) :
salt == Array.Empty<byte>() ? nameof(salt) :
salt2 == Array.Empty<byte>() ? nameof(salt2) :
nameof(password));
var nonce = RndByteSized(ChaChaNonceSize);
var nonce2 = RndByteSized(IvBit / 8);
var cipherText = SecretAeadXChaCha20Poly1305.Encrypt(plaintext, nonce, key);
var cipherTextL2 = await EncryptAsync(cipherText, key2, nonce2, salt3);
Array.Clear(key, 0, key.Length);
Array.Clear(key2, 0, key2.Length);
return nonce.Concat(nonce2).Concat(cipherTextL2).ToArray();
}
catch (CryptographicException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(key2, 0, key2.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(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);
try
{
if (cipherText == Array.Empty<byte>() || salt == Array.Empty<byte>() || salt2 == 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) :
nameof(password));
var nonce = new byte[ChaChaNonceSize];
Buffer.BlockCopy(cipherText, 0, nonce, 0, nonce.Length);
var nonce2 = new byte[IvBit / 8];
Buffer.BlockCopy(cipherText, nonce.Length, nonce2, 0, nonce2.Length);
var cipherResult =
new byte[cipherText.Length - nonce2.Length - nonce.Length];
Buffer.BlockCopy(cipherText, nonce.Length + nonce2.Length, cipherResult, 0,
cipherResult.Length);
var resultL2 = await DecryptAsync(cipherResult, key2, salt3);
var resultL0 = SecretAeadXChaCha20Poly1305.Decrypt(resultL2, nonce, key);
Array.Clear(key, 0, key.Length);
Array.Clear(key2, 0, key2.Length);
return resultL0;
}
catch (CryptographicException ex)
{
Array.Clear(key, 0, key.Length);
Array.Clear(key2, 0, key2.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(password, 0, password.Length);
ErrorLogging.ErrorLog(ex);
return Array.Empty<byte>();
}
}
The code has been tested extensively and works fine.
I’m wondering, what changes can I make to make it more readable, did i implement the use of two ciphers correctly, as well as would splitting the 3 keys from one argon2id be more efficient / secure. And if there’s anything else I can do to improve security. I’m basically trying to make this impossible to break.