Skip to content

Instantly share code, notes, and snippets.

@lukepothier
Last active March 15, 2018 10:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukepothier/1406285a926cbce4a0cf7d31b7cc5301 to your computer and use it in GitHub Desktop.
Save lukepothier/1406285a926cbce4a0cf7d31b7cc5301 to your computer and use it in GitHub Desktop.
AES & HMAC encryption and decryption for UTF-8 strings. Modified from original by Jay Tuley.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace YourNamespace
{
public class EncryptionService : IEncryptionService
{
static readonly RandomNumberGenerator _random = RandomNumberGenerator.Create();
// Preconfigured encryption parameters
public static readonly int BlockBitSize = 128;
public static readonly int KeyBitSize = 256;
// Preconfigured password key derivation parameters
public static readonly int SaltBitSize = 64;
public static readonly int Iterations = 10000;
public static readonly int MinPasswordLength = 12;
/// <summary>
/// Helper that generates a random key on each call.
/// </summary>
/// <returns></returns>
public byte[] GenerateEncryptionKey()
{
byte[] key = new byte[KeyBitSize / 8];
_random.GetBytes(key);
return key;
}
/// <summary>
/// Simple encryption (AES) then authentication (HMAC) for a UTF8 value.
/// </summary>
/// <param name="secretValue">The secret value.</param>
/// <param name="cryptKey">The crypt key.</param>
/// <param name="authKey">The auth key.</param>
/// <param name="nonSecretPayload">(Optional) Non-secret payload.</param>
/// <returns>
/// Encrypted value
/// </returns>
/// <exception cref="System.ArgumentException">Secret value required;secretValue</exception>
/// <remarks>
/// Adds overhead of (optional-payload + blockSize(16) + value-padded-to-blocksize + HMAC-tag(32)) * 1.33 base64
/// </remarks>
public string Encrypt(string secretValue, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null)
{
if (string.IsNullOrEmpty(secretValue))
throw new ArgumentException("Secret value required", nameof(secretValue));
byte[] plainText = Encoding.UTF8.GetBytes(secretValue);
byte[] cipherText = Encrypt(plainText, cryptKey, authKey, nonSecretPayload);
return Convert.ToBase64String(cipherText);
}
/// <summary>
/// Simple authentication (HMAC) then decryption (AES) for a secret UTF8 value.
/// </summary>
/// <param name="encryptedValue">The encrypted value.</param>
/// <param name="cryptKey">The crypt key.</param>
/// <param name="authKey">The auth key.</param>
/// <param name="nonSecretPayloadLength">Length of the non-secret payload.</param>
/// <returns>
/// Decrypted value
/// </returns>
/// <exception cref="System.ArgumentException">Encrypted value required;encryptedValue</exception>
public string Decrypt(string encryptedValue, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0)
{
if (string.IsNullOrWhiteSpace(encryptedValue))
throw new ArgumentException("Encrypted value required", nameof(encryptedValue));
byte[] cipherText = Convert.FromBase64String(encryptedValue);
byte[] plainText = Decrypt(cipherText, cryptKey, authKey, nonSecretPayloadLength);
return plainText == null
? null
: Encoding.UTF8.GetString(plainText);
}
/// <summary>
/// Simple encryption (AES) then authentication (HMAC) for a UTF8 value.
/// </summary>
/// <param name="secretValue">The secret value.</param>
/// <param name="cryptKey">The crypt key.</param>
/// <param name="authKey">The auth key.</param>
/// <param name="nonSecretPayload">(Optional) non-secret payload.</param>
/// <returns>
/// Encrypted value
/// </returns>
/// <remarks>
/// Adds overhead of (optional-payload + blockSize(16) + value-padded-to-blocksize + HMAC-tag(32)) * 1.33 base64
/// </remarks>
static byte[] Encrypt(byte[] secretValue, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null)
{
// User error checks
if (cryptKey == null || cryptKey.Length != KeyBitSize / 8)
throw new ArgumentException($"Key needs to be {KeyBitSize}-bit", nameof(cryptKey));
if (authKey == null || authKey.Length != KeyBitSize / 8)
throw new ArgumentException($"Key needs to be {KeyBitSize}-bit", nameof(authKey));
if (secretValue == null || secretValue.Length < 1)
throw new ArgumentException("Secret value required", nameof(secretValue));
// non-secret payload optional
nonSecretPayload = nonSecretPayload ?? new byte[] { };
byte[] cipherText;
byte[] iv;
using (var aes = new AesManaged
{
KeySize = KeyBitSize,
BlockSize = BlockBitSize,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7
})
{
// Use random IV
aes.GenerateIV();
iv = aes.IV;
using (ICryptoTransform encrypter = aes.CreateEncryptor(cryptKey, iv))
using (var cipherStream = new MemoryStream())
{
using (var cryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
using (var binaryWriter = new BinaryWriter(cryptoStream))
{
// Encrypt data
binaryWriter.Write(secretValue);
}
cipherText = cipherStream.ToArray();
}
}
// Assemble encrypted value and add authentication
using (var hmac = new HMACSHA256(authKey))
using (var encryptedStream = new MemoryStream())
{
using (var binaryWriter = new BinaryWriter(encryptedStream))
{
// Prepend non-secret payload, if any
binaryWriter.Write(nonSecretPayload);
// Prepend IV
binaryWriter.Write(iv);
// Write Ciphertext
binaryWriter.Write(cipherText);
binaryWriter.Flush();
// Authenticate all data
byte[] tag = hmac.ComputeHash(encryptedStream.ToArray());
// Postpend tag
binaryWriter.Write(tag);
}
return encryptedStream.ToArray();
}
}
/// <summary>
/// Simple authentication (HMAC) then decryption (AES) for a secret UTF8 value.
/// </summary>
/// <param name="encryptedValue">The encrypted value.</param>
/// <param name="cryptKey">The crypt key.</param>
/// <param name="authKey">The auth key.</param>
/// <param name="nonSecretPayloadLength">Length of the non-secret payload.</param>
/// <returns>Decrypted value</returns>
static byte[] Decrypt(byte[] encryptedValue, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0)
{
// Basic usage error checks
if (cryptKey == null || cryptKey.Length != KeyBitSize / 8)
throw new ArgumentException($"CryptKey needs to be {KeyBitSize}-bit", nameof(cryptKey));
if (authKey == null || authKey.Length != KeyBitSize / 8)
throw new ArgumentException($"AuthKey needs to be {KeyBitSize}-bit", nameof(authKey));
if (encryptedValue == null || encryptedValue.Length == 0)
throw new ArgumentException("Encrypted value required", nameof(encryptedValue));
using (var hmac = new HMACSHA256(authKey))
{
byte[] sentTag = new byte[hmac.HashSize / 8];
// Calculate tag
byte[] calcTag = hmac.ComputeHash(encryptedValue, 0, encryptedValue.Length - sentTag.Length);
var ivLength = (BlockBitSize / 8);
// If value length is too small just return null
if (encryptedValue.Length < sentTag.Length + nonSecretPayloadLength + ivLength)
return new byte[0];
// Copy sent tag
Array.Copy(encryptedValue, encryptedValue.Length - sentTag.Length, sentTag, 0, sentTag.Length);
// Compare tag with constant time comparison
var compare = 0;
for (var i = 0; i < sentTag.Length; i++)
compare |= sentTag[i] ^ calcTag[i];
// If value doesn't authenticate return null
if (compare != 0)
return new byte[0];
using (var aes = new AesManaged
{
KeySize = KeyBitSize,
BlockSize = BlockBitSize,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7
})
{
// Copy IV from value
byte[] iv = new byte[ivLength];
Array.Copy(encryptedValue, nonSecretPayloadLength, iv, 0, iv.Length);
using (ICryptoTransform decrypter = aes.CreateDecryptor(cryptKey, iv))
using (var plainTextStream = new MemoryStream())
{
using (var decrypterStream = new CryptoStream(plainTextStream, decrypter, CryptoStreamMode.Write))
using (var binaryWriter = new BinaryWriter(decrypterStream))
{
// Decrypt cipher text from value
binaryWriter.Write(
encryptedValue,
nonSecretPayloadLength + iv.Length,
encryptedValue.Length - nonSecretPayloadLength - iv.Length - sentTag.Length
);
}
// Return plain text
return plainTextStream.ToArray();
}
}
}
}
}
}
namespace YourNamespace
{
public interface IEncryptionService
{
byte[] GenerateEncryptionKey();
string Encrypt(string secretValue, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null);
string Decrypt(string encryptedValue, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment