Last active
March 15, 2018 10:09
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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