I've been tasked with creating some way to encrypt arbitrary text for a .NET Core app, with the following requirements:
- Messages will be between 10 bytes and 1 kB.
- Boss doesn't want me to use AES-GCM (which I know would be the sensible option as it's already included in .NET Core and I've already read that implementing your own encryption is a bad idea) and wants me to come up with a custom implementation.
- This implementation will not be used too much, usually only at the app's startup.
After some reading, I've come up with the following:
Encryption:
- Convert message to UTF-8 bytes (
plaintext
) - Generate a random
salt
. - Using
salt
, derive enough material with PBKDF2 for anaes_key
and anhmac_key
. - Generate a random
iv
. - AES-CBC encrypt
plaintext
withaes_key
andiv
(ciphertext
). - Compute an HMAC-SHA-256
hash
ofciphertext||iv
. - Return
salt||hash||iv||ciphertext
(payload
).
Decryption:
- Extract
salt
,hash
,iv
,ciphertext
frompayload
. - Using
salt
, derive enough material with PBKDF2 for anaes_key
and anhmac_key
. - Compute an HMAC-SHA-256
computed_hash
ofciphertext||iv
. - If
computed_hash
andhash
are different, rejectpayload
. - AES-CBC decrypt
ciphertext
withaes_key
andiv
(plaintext
). - Convert
plaintext
to UTF-8 string (message
). - Return
message
.
The implementation is as follows:
public class AuthenticatedEncryptionHelper
{
private const int _SALT_LENGTH = 16;
private const int _KEY_DERIVATION_ITERATIONS = 100000;
private const int _CRYPTO_KEY_LENGTH = 16;
private const int _HMAC_KEY_LENGTH = 16;
private const int _IV_LENGTH = 16;
public byte[] Encrypt(string message, string password)
{
var salt = GenerateRandomBytes(_SALT_LENGTH);
var (enc_key, hmac_key) = DeriveKeys(password, salt);
var iv = GenerateRandomBytes(_IV_LENGTH);
var plaintext = Encoding.UTF8.GetBytes(message);
var ciphertext = AesEncrypt(plaintext, enc_key, iv);
var hmac = ComputeHmac(ciphertext, iv, hmac_key);
var payload = GeneratePayload(salt, hmac, iv, ciphertext);
return payload;
}
public string Decrypt(byte[] payload, string password)
{
var (salt, hmac, iv, ciphertext) = ExplodePayload(payload);
var (dec_key, hmac_key) = DeriveKeys(password, salt);
var computed_hmac = ComputeHmac(ciphertext, iv, hmac_key);
if (!AreHmacsEqual(hmac, computed_hmac))
{
throw new Exception();
}
var plaintext = AesDecrypt(ciphertext, dec_key, iv);
var message = Encoding.UTF8.GetString(plaintext);
return message;
}
private byte[] GenerateRandomBytes(int length)
{
var result = new byte[length];
using (var r = RandomNumberGenerator.Create())
{
r.GetBytes(result);
}
return result;
}
private (byte[], byte[]) DeriveKeys(string password, byte[] salt)
{
var length = _CRYPTO_KEY_LENGTH + _HMAC_KEY_LENGTH;
var result = new byte[length];
using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, _KEY_DERIVATION_ITERATIONS))
{
result = pbkdf2.GetBytes(length);
}
var r1 = new byte[_CRYPTO_KEY_LENGTH];
var r2 = new byte[_HMAC_KEY_LENGTH];
Buffer.BlockCopy(result, 0, r1, 0, _CRYPTO_KEY_LENGTH);
Buffer.BlockCopy(result, _CRYPTO_KEY_LENGTH, r2, 0, _HMAC_KEY_LENGTH);
return (r1, r2);
}
private byte[] AesEncrypt(byte[] plaintext, byte[] key, byte[] iv)
{
byte[] ciphertext;
using (var aes = Aes.Create())
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
using (var encryptor = aes.CreateEncryptor())
{
ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
}
}
return ciphertext;
}
private byte[] ComputeHmac(byte[] ciphertext, byte[] iv, byte[] key)
{
var input = new byte[iv.Length + ciphertext.Length];
Buffer.BlockCopy(iv, 0, input, 0, iv.Length);
Buffer.BlockCopy(ciphertext, 0, input, iv.Length, ciphertext.Length);
byte[] result;
using (var hash = new HMACSHA256(key))
{
result = hash.ComputeHash(input);
}
return result;
}
private byte[] GeneratePayload(byte[] salt, byte[] hmac, byte[] iv, byte[] ciphertext)
{
var result = new byte[salt.Length + hmac.Length + iv.Length + ciphertext.Length];
var curr_index = 0;
Buffer.BlockCopy(salt, 0, result, curr_index, salt.Length);
curr_index += salt.Length;
Buffer.BlockCopy(hmac, 0, result, curr_index, hmac.Length);
curr_index += hmac.Length;
Buffer.BlockCopy(iv, 0, result, curr_index, iv.Length);
curr_index += iv.Length;
Buffer.BlockCopy(ciphertext, 0, result, curr_index, ciphertext.Length);
return result;
}
private (byte[], byte[], byte[], byte[]) ExplodePayload(byte[] payload)
{
var salt = new byte[_SALT_LENGTH];
var hmac = new byte[32];
var iv = new byte[_IV_LENGTH];
var ciphertext = new byte[payload.Length - (_SALT_LENGTH + 32 + _IV_LENGTH)];
var curr_index = 0;
Buffer.BlockCopy(payload, curr_index, salt, 0, _SALT_LENGTH);
curr_index += _SALT_LENGTH;
Buffer.BlockCopy(payload, curr_index, hmac, 0, 32);
curr_index += 32;
Buffer.BlockCopy(payload, curr_index, iv, 0, _IV_LENGTH);
curr_index += _IV_LENGTH;
Buffer.BlockCopy(payload, curr_index, ciphertext, 0, ciphertext.Length);
return (salt, hmac, iv, ciphertext);
}
private bool AreHmacsEqual(byte[] hmac, byte[] computedHmac)
{
var result = true;
if (hmac.Length != computedHmac.Length)
{
result = false;
}
for (int i = 0; i < hmac.Length; i++)
{
if (hmac[i] != computedHmac[i])
{
result = false;
}
}
return result;
}
private byte[] AesDecrypt(byte[] ciphertext, byte[] key, byte[] iv)
{
byte[] plaintext;
using (var aes = Aes.Create())
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor())
{
plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
}
}
return plaintext;
}
}
Any criticism of this code (or the algorithm) will be greatly appreciated.