Skip to content

Instantly share code, notes, and snippets.

@Jaecen
Created October 6, 2017 23:38
Show Gist options
  • Save Jaecen/0fef3f73d037532e7b2b31092ef5d5d5 to your computer and use it in GitHub Desktop.
Save Jaecen/0fef3f73d037532e7b2b31092ef5d5d5 to your computer and use it in GitHub Desktop.
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace AesKeySizes
{
// This class holds our encryption and authentication keys. We use a class just to make it
// easier to pass these values around.
class KeyConfig
{
public byte[] EncryptionKey { get; }
public byte[] AuthenticationKey { get; }
public KeyConfig(
byte[] encryptionKey,
byte[] authenticationKey)
{
EncryptionKey = encryptionKey ?? throw new ArgumentNullException(nameof(encryptionKey));
AuthenticationKey = authenticationKey ?? throw new ArgumentNullException(nameof(authenticationKey));
}
}
class Program
{
static void Main(string[] args)
{
// This class shows the correct way to encrypt and decrypt a value
// using AES. It demonstrates the following:
// - How to use a key derivation function (KDF) to turn a human-readable
// password into unguessable encryption and authentication keys.
// - How to configure AES for this purpose.
// - How to encrypt and decrypt AES.
// - How to generate and communicate a unique IV for every message.
// - How to configure HMAC for authenticating AES messages.
// - How to sign and authenticate an encrypted message.
// First, we will set up the external values that are needed.
// The password is a human-readable secret of any length. It would be wise
// to enforce a reasonable minimum length. This is used for deriving encryption
// and authentication keys.
var password = "Super Secret!";
// The salt is a 64-bit number that must be unique for every instance of
// the software. This is also used for deriving keys from the password.
var salt = Convert.FromBase64String("mgPDhRu4GYU=");
// The value text is what we're going to encrypt.
var valueText = "Hello, AES!";
// Next, we will use the password and salt to derive encryption and
// authentication keys.
var keyConfig = DeriveKeys(password, salt);
// We use the keys to encrypt the value
var value = Encoding.UTF8.GetBytes(valueText);
var encryptedMessage = Encrypt(keyConfig, value);
// Then we use the keys and encrypted value to decrypt the message
var result = Decrypt(keyConfig, encryptedMessage);
var resultText = Encoding.UTF8.GetString(result);
// Finally, we show the result is the same as the input
Console.WriteLine();
Console.WriteLine($"Input: {value.AsString()}");
Console.WriteLine($" {valueText}");
Console.WriteLine();
Console.WriteLine($"Output: {result.AsString()}");
Console.WriteLine($" {resultText}");
Console.WriteLine();
Console.WriteLine($"Input == Output? {valueText == resultText}");
}
static KeyConfig DeriveKeys(string password, byte[] salt)
{
// This method uses a password-based key derivation function (PBKDF) to turn a
// human-friendly password into an unguessable set of keys. We generate one
// large key from the password, then split it into separate encryption and
// authentication keys.
var passwordBytes = Encoding.UTF8.GetBytes(password);
var encryptionKey = new byte[32];
var authenticationKey = new byte[64];
// Generate a master key
var masterKey = new Rfc2898DeriveBytes(passwordBytes, salt, 2048)
.GetBytes(encryptionKey.Length + authenticationKey.Length);
// Separate the master key into separate keys
Array.ConstrainedCopy(masterKey, 0, encryptionKey, 0, encryptionKey.Length);
Array.ConstrainedCopy(masterKey, encryptionKey.Length, authenticationKey, 0, authenticationKey.Length);
return new KeyConfig(
encryptionKey,
authenticationKey);
}
static byte[] Encrypt(KeyConfig config, byte[] value)
{
// When using AES in CBC mode, you must prevent an attacker from modifying
// the padding at the end of the message. If you attempt to decrypt any
// message you receive, an attacker can modify the last byte of the message
// and, based on the server's response, eventually read part of the
// encrypted message. This is known as a padding oracle attack.
// To prevent an attacker from modifying the payload, we have to use some
// sort of authentication mechanism. In this case, we've decided to use HMAC.
// We use the HMAC to cryptographically sign the encrypted message. If an
// attacker attempt to modify the payload, we will detect it before trying
// to decrypt the message and revealing information about the key to the
// attacker.
// Create an AES instance for encryption
byte[] ivAndCiphertext;
using(var aes = CreateAes(config.EncryptionKey))
{
// We must generate a random IV for every encrypted message. The IV
// will be stored in the message.
aes.GenerateIV();
Console.WriteLine($"[Encrypt] Generated IV");
Console.WriteLine($" {aes.IV.AsString(),32}");
// Encrypt the value using AES
byte[] ciphertext;
using(var memoryStream = new MemoryStream())
{
using(var encryptor = aes.CreateEncryptor())
using(var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
using(var binaryWriter = new BinaryWriter(cryptoStream))
{
binaryWriter.Write(value.Length);
binaryWriter.Write(value);
Console.WriteLine($"[Encrypt] Encrypted {value.Length} bytes");
}
ciphertext = memoryStream.ToArray();
}
// Bundle IV with the ciphertext. We prepend it in front of the ciphertext.
using(var memoryStream = new MemoryStream())
{
using(var binaryWriter = new BinaryWriter(memoryStream))
{
binaryWriter.Write(aes.IV.Length);
binaryWriter.Write(aes.IV);
binaryWriter.Write(ciphertext.Length);
binaryWriter.Write(ciphertext);
}
ivAndCiphertext = memoryStream.ToArray();
}
}
// Create an HMAC instance for authentication.
byte[] mac;
using(var hmac = CreateHmac(config.AuthenticationKey))
{
// Generate a signature of the ciphertext and the IV. This guarantees that
// the message and IV are not tampered with, along with protecting us from
// padding oracle attacks against our encryption key.
mac = hmac.ComputeHash(ivAndCiphertext);
Console.WriteLine($"[Encrypt] Generated MAC");
Console.WriteLine($" {mac.AsString(),64}");
}
// Generate the final payload that includes the MAC, IV, and ciphertext. The MAC
// is prepended to the front of the IV and ciphertext.
using(var memoryStream = new MemoryStream())
{
using(var binaryWriter = new BinaryWriter(memoryStream))
{
binaryWriter.Write(mac.Length);
binaryWriter.Write(mac);
binaryWriter.Write(ivAndCiphertext);
}
return memoryStream.ToArray();
}
}
static byte[] Decrypt(KeyConfig config, byte[] value)
{
// This essentially performs the encryption steps in reverse.
// Read and validate the MAC. If the payload has been modified, we'll catch it here.
using(var memoryStream = new MemoryStream(value))
{
// Read the MAC
byte[] storedMac;
using(var binaryReader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: true))
{
var macSize = binaryReader.ReadInt32();
storedMac = binaryReader.ReadBytes(macSize);
Console.WriteLine($"[Decrypt] Read MAC");
Console.WriteLine($" {storedMac.AsString(),64}");
}
// Save the position in the stream so we can rewind to it later
var ivPosition = memoryStream.Position;
// Use the HMAC to read the rest of the stream, sign it, and ensure
// the signature matches the one received in the message.
using(var hmac = CreateHmac(config.AuthenticationKey))
{
var computedMac = hmac.ComputeHash(memoryStream);
Console.WriteLine($"[Decrypt] Computed MAC");
Console.WriteLine($" {computedMac.AsString(),64}");
if(!Enumerable.SequenceEqual(computedMac, storedMac))
throw new InvalidOperationException("Invalid MAC");
}
// Rewind the stream so we can read the rest of it
memoryStream.Position = ivPosition;
// Read the IV and ciphertext size. We don't actually use the ciphertext
// size, but it's helpful to have for other systems and for diagnostics.
byte[] iv;
int ciphertextSize;
using(var binaryReader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: true))
{
var ivSize = binaryReader.ReadInt32();
iv = binaryReader.ReadBytes(ivSize);
Console.WriteLine($"[Decrypt] Read IV");
Console.WriteLine($" {iv.AsString(),32}");
ciphertextSize = binaryReader.ReadInt32();
}
// Create an AES instance to decrypt the ciphertext. Use encryption
// key derived from the password and the IV that we just read off the
// message.
using(var aes = CreateAes(config.EncryptionKey, iv))
using(var decryptor = aes.CreateDecryptor())
using(var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
using(var binaryReader = new BinaryReader(cryptoStream))
{
// Decrypt and return the message
var plaintextSize = binaryReader.ReadInt32();
Console.WriteLine($"[Decrypt] Decrypting {plaintextSize} bytes");
return binaryReader.ReadBytes(plaintextSize);
}
}
}
// Create an AES instance configured with a 256-bit key, CBC mode,
// and PKCS7 padding.
static Aes CreateAes(byte[] encryptionKey)
=> new AesManaged
{
KeySize = 256,
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7,
Key = encryptionKey,
};
// Create an AES instance as above, but with the given IV.
static Aes CreateAes(byte[] encryptionKey, byte[] iv)
{
var aes = CreateAes(encryptionKey);
aes.IV = iv;
return aes;
}
// Create an HMAC instance configured to use SHA256 and the given key.
static HMAC CreateHmac(byte[] authenticationKey)
=> new HMACSHA256
{
Key = authenticationKey,
};
}
public static class ByteArrayExtensions
{
// Make byte arrays human-readable for diagnostics
public static string AsString(this byte[] bytes)
=> string.Join(string.Empty, bytes.Select(b => b.ToString("x2")));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment