Created
October 6, 2017 23:38
-
-
Save Jaecen/0fef3f73d037532e7b2b31092ef5d5d5 to your computer and use it in GitHub Desktop.
This file contains hidden or 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.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