Modern Examples of Symmetric Authenticated C# with Example
Encryption of a string Cryptography is something very hard and after spending a lot of time reading different examples and seeing how easy it is to introduce some form of vulnerability I found an answer originally written by @jbtule that I think is very good. Enjoy reading: "The general best practice for symmetric encryption is to use Authenticated Encryption with Associated Data (AEAD), however this isn't a part of the standard .net crypto libraries. So the first example uses AES256 and then HMAC256, a two step Encrypt then MAC, which requires more overhead and more keys. The second example uses the simpler practice of AES256-GCM using the open source Bouncy Castle (via nuget). Both examples have a main function that takes secret message string, key(s) and an optional non-secret payload and return and authenticated encrypted string optionally prepended with the non-secret data. Ideally you would use these with 256bit key(s) randomly generated see NewKey(). Both examples also have a helper methods that use a string password to generate the keys. These helper methods are provided as a convenience to match up with other examples, however they are far less secure because the strength of the password is going to be far weaker than a 256 bit key. Update: Added byte[] overloads, and only the Gist has the full formatting with 4 spaces indent and api docs due to StackOverflow answer limits." .NET Built-in Encrypt(AES)-Then-MAC(HMAC) [Gist] /* * This work (Modern Encryption of a String C#, by James Tuley), * identified by James Tuley, is free of known copyright restrictions. * https://gist.github.com/4336842 * http://creativecommons.org/publicdomain/mark/1.0/ */ using System; using System.IO; using System.Security.Cryptography; using System.Text; namespace Encryption { public static class AESThenHMAC { private 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; /// /// Helper that generates a random key on each call. /// /// public static byte[] NewKey() { var key = new byte[KeyBitSize / 8]; Random.GetBytes(key); return key; } /// /// Simple Encryption (AES) then Authentication (HMAC) for a UTF8 Message. /// /// The secret message. /// The crypt key. /// The auth key. /// (Optional) Non-Secret Payload. /// /// Encrypted Message /// /// Secret Message Required!;secretMessage /// /// Adds overhead of (Optional-Payload + BlockSize(16) + Message-Padded-To-Blocksize + HMac- Tag(32)) * 1.33 Base64 /// public static string SimpleEncrypt(string secretMessage, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null) { if (string.IsNullOrEmpty(secretMessage)) throw new ArgumentException("Secret Message Required!", "secretMessage"); var plainText = Encoding.UTF8.GetBytes(secretMessage); var cipherText = SimpleEncrypt(plainText, cryptKey, authKey, nonSecretPayload); return Convert.ToBase64String(cipherText); } /// /// Simple Authentication (HMAC) then Decryption (AES) for a secrets UTF8 Message. /// /// The encrypted message. /// The crypt key. /// The auth key. /// Length of the non secret payload. /// /// Decrypted Message /// /// Encrypted Message Required!;encryptedMessage public static string SimpleDecrypt(string encryptedMessage, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0) { if (string.IsNullOrWhiteSpace(encryptedMessage)) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); var cipherText = Convert.FromBase64String(encryptedMessage); var plainText = SimpleDecrypt(cipherText, cryptKey, authKey, nonSecretPayloadLength); return plainText == null ? null : Encoding.UTF8.GetString(plainText); } /// /// Simple Encryption (AES) then Authentication (HMAC) of a UTF8 message /// using Keys derived from a Password (PBKDF2). /// /// The secret message. /// The password. /// The non secret payload. /// /// Encrypted Message /// /// password /// /// Significantly less secure than using random binary keys. /// Adds additional non secret payload for key generation parameters. /// public static string SimpleEncryptWithPassword(string secretMessage, string password, byte[] nonSecretPayload = null) { if (string.IsNullOrEmpty(secretMessage)) throw new ArgumentException("Secret Message Required!", "secretMessage"); var plainText = Encoding.UTF8.GetBytes(secretMessage); var cipherText = SimpleEncryptWithPassword(plainText, password, nonSecretPayload); return Convert.ToBase64String(cipherText); } /// /// Simple Authentication (HMAC) and then Descryption (AES) of a UTF8 Message /// using keys derived from a password (PBKDF2). /// /// The encrypted message. /// The password. /// Length of the non secret payload. /// /// Decrypted Message /// /// Encrypted Message Required!;encryptedMessage /// /// Significantly less secure than using random binary keys. /// public static string SimpleDecryptWithPassword(string encryptedMessage, string password, int nonSecretPayloadLength = 0) { if (string.IsNullOrWhiteSpace(encryptedMessage)) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); var cipherText = Convert.FromBase64String(encryptedMessage); var plainText = SimpleDecryptWithPassword(cipherText, password, nonSecretPayloadLength); return plainText == null ? null : Encoding.UTF8.GetString(plainText); } public static byte[] SimpleEncrypt(byte[] secretMessage, byte[] cryptKey, byte[] authKey, byte[] nonSecretPayload = null) { //User Error Checks if (cryptKey == null || cryptKey.Length != KeyBitSize / 8) throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "cryptKey"); if (authKey == null || authKey.Length != KeyBitSize / 8) throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "authKey"); if (secretMessage == null || secretMessage.Length < 1) throw new ArgumentException("Secret Message Required!", "secretMessage"); //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 (var 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(secretMessage); } cipherText = cipherStream.ToArray(); } } //Assemble encrypted message 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 var tag = hmac.ComputeHash(encryptedStream.ToArray()); //Postpend tag binaryWriter.Write(tag); } return encryptedStream.ToArray(); } } public static byte[] SimpleDecrypt(byte[] encryptedMessage, byte[] cryptKey, byte[] authKey, int nonSecretPayloadLength = 0) { //Basic Usage Error Checks if (cryptKey == null || cryptKey.Length != KeyBitSize / 8) throw new ArgumentException(String.Format("CryptKey needs to be {0} bit!", KeyBitSize), "cryptKey"); if (authKey == null || authKey.Length != KeyBitSize / 8) throw new ArgumentException(String.Format("AuthKey needs to be {0} bit!", KeyBitSize), "authKey"); if (encryptedMessage == null || encryptedMessage.Length == 0) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); using (var hmac = new HMACSHA256(authKey)) { var sentTag = new byte[hmac.HashSize / 8]; //Calculate Tag var calcTag = hmac.ComputeHash(encryptedMessage, 0, encryptedMessage.Length - sentTag.Length); var ivLength = (BlockBitSize / 8); //if message length is to small just return null if (encryptedMessage.Length < sentTag.Length + nonSecretPayloadLength + ivLength) return null; //Grab Sent Tag Array.Copy(encryptedMessage, encryptedMessage.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 message doesn't authenticate return null if (compare != 0) return null; using (var aes = new AesManaged { KeySize = KeyBitSize, BlockSize = BlockBitSize, Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7 }) { //Grab IV from message var iv = new byte[ivLength]; Array.Copy(encryptedMessage, nonSecretPayloadLength, iv, 0, iv.Length); using (var 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 Message binaryWriter.Write( encryptedMessage, nonSecretPayloadLength + iv.Length, encryptedMessage.Length - nonSecretPayloadLength - iv.Length - sentTag.Length ); } //Return Plain Text return plainTextStream.ToArray(); } } } } public static byte[] SimpleEncryptWithPassword(byte[] secretMessage, string password, byte[] nonSecretPayload = null) { nonSecretPayload = nonSecretPayload ?? new byte[] {}; //User Error Checks if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength) throw new ArgumentException(String.Format("Must have a password of at least {0} characters!", MinPasswordLength), "password"); if (secretMessage == null || secretMessage.Length ==0) throw new ArgumentException("Secret Message Required!", "secretMessage"); var payload = new byte[((SaltBitSize / 8) * 2) + nonSecretPayload.Length]; Array.Copy(nonSecretPayload, payload, nonSecretPayload.Length); int payloadIndex = nonSecretPayload.Length; byte[] cryptKey; byte[] authKey; //Use Random Salt to prevent pre-generated weak password attacks. using (var generator = new Rfc2898DeriveBytes(password, SaltBitSize / 8, Iterations)) { var salt = generator.Salt; //Generate Keys cryptKey = generator.GetBytes(KeyBitSize / 8); //Create Non Secret Payload Array.Copy(salt, 0, payload, payloadIndex, salt.Length); payloadIndex += salt.Length; } //Deriving separate key, might be less efficient than using HKDF, //but now compatible with RNEncryptor which had a very similar wireformat and requires less code than HKDF. using (var generator = new Rfc2898DeriveBytes(password, SaltBitSize / 8, Iterations)) { var salt = generator.Salt; //Generate Keys authKey = generator.GetBytes(KeyBitSize / 8); //Create Rest of Non Secret Payload Array.Copy(salt, 0, payload, payloadIndex, salt.Length); } return SimpleEncrypt(secretMessage, cryptKey, authKey, payload); } public static byte[] SimpleDecryptWithPassword(byte[] encryptedMessage, string password, int nonSecretPayloadLength = 0) { //User Error Checks if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength) throw new ArgumentException(String.Format("Must have a password of at least {0} characters!", MinPasswordLength), "password"); if (encryptedMessage == null || encryptedMessage.Length == 0) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); var cryptSalt = new byte[SaltBitSize / 8]; var authSalt = new byte[SaltBitSize / 8]; //Grab Salt from Non-Secret Payload Array.Copy(encryptedMessage, nonSecretPayloadLength, cryptSalt, 0, cryptSalt.Length); Array.Copy(encryptedMessage, nonSecretPayloadLength + cryptSalt.Length, authSalt, 0, authSalt.Length); byte[] cryptKey; byte[] authKey; //Generate crypt key using (var generator = new Rfc2898DeriveBytes(password, cryptSalt, Iterations)) { cryptKey = generator.GetBytes(KeyBitSize / 8); } //Generate auth key using (var generator = new Rfc2898DeriveBytes(password, authSalt, Iterations)) { authKey = generator.GetBytes(KeyBitSize / 8); } return SimpleDecrypt(encryptedMessage, cryptKey, authKey, cryptSalt.Length + authSalt.Length + nonSecretPayloadLength); } } } Bouncy Castle AES-GCM [Gist] /* * This work (Modern Encryption of a String C#, by James Tuley), * identified by James Tuley, is free of known copyright restrictions. * https://gist.github.com/4336842 * http://creativecommons.org/publicdomain/mark/1.0/ */ using System; using System.IO; using System.Text; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; namespace Encryption { public static class AESGCM { private static readonly SecureRandom Random = new SecureRandom(); //Preconfigured Encryption Parameters public static readonly int NonceBitSize = 128; public static readonly int MacBitSize = 128; public static readonly int KeyBitSize = 256; //Preconfigured Password Key Derivation Parameters public static readonly int SaltBitSize = 128; public static readonly int Iterations = 10000; public static readonly int MinPasswordLength = 12; /// /// Helper that generates a random new key on each call. /// /// public static byte[] NewKey() { var key = new byte[KeyBitSize / 8]; Random.NextBytes(key); return key; } /// /// Simple Encryption And Authentication (AES-GCM) of a UTF8 string. /// /// The secret message. /// The key. /// Optional non-secret payload. /// /// Encrypted Message /// /// Secret Message Required!;secretMessage /// /// Adds overhead of (Optional-Payload + BlockSize(16) + Message + HMac-Tag(16)) * 1.33 Base64 /// public static string SimpleEncrypt(string secretMessage, byte[] key, byte[] nonSecretPayload = null) { if (string.IsNullOrEmpty(secretMessage)) throw new ArgumentException("Secret Message Required!", "secretMessage"); var plainText = Encoding.UTF8.GetBytes(secretMessage); var cipherText = SimpleEncrypt(plainText, key, nonSecretPayload); return Convert.ToBase64String(cipherText); } /// /// Simple Decryption & Authentication (AES-GCM) of a UTF8 Message /// /// The encrypted message. /// The key. /// Length of the optional non-secret payload. /// Decrypted Message public static string SimpleDecrypt(string encryptedMessage, byte[] key, int nonSecretPayloadLength = 0) { if (string.IsNullOrEmpty(encryptedMessage)) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); var cipherText = Convert.FromBase64String(encryptedMessage); var plainText = SimpleDecrypt(cipherText, key, nonSecretPayloadLength); return plainText == null ? null : Encoding.UTF8.GetString(plainText); } /// /// Simple Encryption And Authentication (AES-GCM) of a UTF8 String /// using key derived from a password (PBKDF2). /// /// The secret message. /// The password. /// The non secret payload. /// /// Encrypted Message /// /// /// Significantly less secure than using random binary keys. /// Adds additional non secret payload for key generation parameters. /// public static string SimpleEncryptWithPassword(string secretMessage, string password, byte[] nonSecretPayload = null) { if (string.IsNullOrEmpty(secretMessage)) throw new ArgumentException("Secret Message Required!", "secretMessage"); var plainText = Encoding.UTF8.GetBytes(secretMessage); var cipherText = SimpleEncryptWithPassword(plainText, password, nonSecretPayload); return Convert.ToBase64String(cipherText); } /// /// Simple Decryption and Authentication (AES-GCM) of a UTF8 message /// using a key derived from a password (PBKDF2) /// /// The encrypted message. /// The password. /// Length of the non secret payload. /// /// Decrypted Message /// /// Encrypted Message Required!;encryptedMessage /// /// Significantly less secure than using random binary keys. /// public static string SimpleDecryptWithPassword(string encryptedMessage, string password, int nonSecretPayloadLength = 0) { if (string.IsNullOrWhiteSpace(encryptedMessage)) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); var cipherText = Convert.FromBase64String(encryptedMessage); var plainText = SimpleDecryptWithPassword(cipherText, password, nonSecretPayloadLength); return plainText == null ? null : Encoding.UTF8.GetString(plainText); } public static byte[] SimpleEncrypt(byte[] secretMessage, byte[] key, byte[] nonSecretPayload = null) { //User Error Checks if (key == null || key.Length != KeyBitSize / 8) throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "key"); if (secretMessage == null || secretMessage.Length == 0) throw new ArgumentException("Secret Message Required!", "secretMessage"); //Non-secret Payload Optional nonSecretPayload = nonSecretPayload ?? new byte[] { }; //Using random nonce large enough not to repeat var nonce = new byte[NonceBitSize / 8]; Random.NextBytes(nonce, 0, nonce.Length); var cipher = new GcmBlockCipher(new AesFastEngine()); var parameters = new AeadParameters(new KeyParameter(key), MacBitSize, nonce, nonSecretPayload); cipher.Init(true, parameters); //Generate Cipher Text With Auth Tag var cipherText = new byte[cipher.GetOutputSize(secretMessage.Length)]; var len = cipher.ProcessBytes(secretMessage, 0, secretMessage.Length, cipherText, 0); cipher.DoFinal(cipherText, len); //Assemble Message using (var combinedStream = new MemoryStream()) { using (var binaryWriter = new BinaryWriter(combinedStream)) { //Prepend Authenticated Payload binaryWriter.Write(nonSecretPayload); //Prepend Nonce binaryWriter.Write(nonce); //Write Cipher Text binaryWriter.Write(cipherText); } return combinedStream.ToArray(); } } public static byte[] SimpleDecrypt(byte[] encryptedMessage, byte[] key, int nonSecretPayloadLength = 0) { //User Error Checks if (key == null || key.Length != KeyBitSize / 8) throw new ArgumentException(String.Format("Key needs to be {0} bit!", KeyBitSize), "key"); if (encryptedMessage == null || encryptedMessage.Length == 0) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); using (var cipherStream = new MemoryStream(encryptedMessage)) using (var cipherReader = new BinaryReader(cipherStream)) { //Grab Payload var nonSecretPayload = cipherReader.ReadBytes(nonSecretPayloadLength); //Grab Nonce var nonce = cipherReader.ReadBytes(NonceBitSize / 8); var cipher = new GcmBlockCipher(new AesFastEngine()); var parameters = new AeadParameters(new KeyParameter(key), MacBitSize, nonce, nonSecretPayload); cipher.Init(false, parameters); //Decrypt Cipher Text var cipherText = cipherReader.ReadBytes(encryptedMessage.Length - nonSecretPayloadLength - nonce.Length); var plainText = new byte[cipher.GetOutputSize(cipherText.Length)]; try { var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0); cipher.DoFinal(plainText, len); } catch (InvalidCipherTextException) { //Return null if it doesn't authenticate return null; } return plainText; } } public static byte[] SimpleEncryptWithPassword(byte[] secretMessage, string password, byte[] nonSecretPayload = null) { nonSecretPayload = nonSecretPayload ?? new byte[] {}; //User Error Checks if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength) throw new ArgumentException(String.Format("Must have a password of at least {0} characters!", MinPasswordLength), "password"); if (secretMessage == null || secretMessage.Length == 0) throw new ArgumentException("Secret Message Required!", "secretMessage"); var generator = new Pkcs5S2ParametersGenerator(); //Use Random Salt to minimize pre-generated weak password attacks. var salt = new byte[SaltBitSize / 8]; Random.NextBytes(salt); generator.Init( PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()), salt, Iterations); //Generate Key var key = (KeyParameter)generator.GenerateDerivedMacParameters(KeyBitSize); //Create Full Non Secret Payload var payload = new byte[salt.Length + nonSecretPayload.Length]; Array.Copy(nonSecretPayload, payload, nonSecretPayload.Length); Array.Copy(salt,0, payload,nonSecretPayload.Length, salt.Length); return SimpleEncrypt(secretMessage, key.GetKey(), payload); } public static byte[] SimpleDecryptWithPassword(byte[] encryptedMessage, string password, int nonSecretPayloadLength = 0) { //User Error Checks if (string.IsNullOrWhiteSpace(password) || password.Length < MinPasswordLength) throw new ArgumentException(String.Format("Must have a password of at least {0} characters!", MinPasswordLength), "password"); if (encryptedMessage == null || encryptedMessage.Length == 0) throw new ArgumentException("Encrypted Message Required!", "encryptedMessage"); var generator = new Pkcs5S2ParametersGenerator(); //Grab Salt from Payload var salt = new byte[SaltBitSize / 8]; Array.Copy(encryptedMessage, nonSecretPayloadLength, salt, 0, salt.Length); generator.Init( PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()), salt, Iterations); //Generate Key var key = (KeyParameter)generator.GenerateDerivedMacParameters(KeyBitSize); return SimpleDecrypt(encryptedMessage, key.GetKey(), salt.Length + nonSecretPayloadLength); } } }