public
Last active

I have two code examples that I wrote for best practices encrypting a string in c#. They are both using authenticated encryption. One is using the bouncy castle api to do AES-GCM which is the better practice of the two and the other is for the limitation of only using the built in .NET api so it does encryption then authentication separately. However each has a less secure helper method that uses a string password to derive the bytes for the key with salt and iterations. Those salt and iterations are then passed in the clear using the authenticated payload. Feedback welcome.

  • Download Gist
AESGCM.cs
C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
/*
* 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;
 
 
/// <summary>
/// Helper that generates a random new key on each call.
/// </summary>
/// <returns></returns>
public static byte[] NewKey()
{
var key = new byte[KeyBitSize / 8];
Random.NextBytes(key);
return key;
}
 
/// <summary>
/// Simple Encryption And Authentication (AES-GCM) of a UTF8 string.
/// </summary>
/// <param name="secretMessage">The secret message.</param>
/// <param name="key">The key.</param>
/// <param name="nonSecretPayload">Optional non-secret payload.</param>
/// <returns>
/// Encrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Secret Message Required!;secretMessage</exception>
/// <remarks>
/// Adds overhead of (Optional-Payload + BlockSize(16) + Message + HMac-Tag(16)) * 1.33 Base64
/// </remarks>
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);
}
 
 
/// <summary>
/// Simple Decryption & Authentication (AES-GCM) of a UTF8 Message
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</param>
/// <param name="key">The key.</param>
/// <param name="nonSecretPayloadLength">Length of the optional non-secret payload.</param>
/// <returns>Decrypted Message</returns>
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 Encoding.UTF8.GetString(plaintext);
}
 
/// <summary>
/// Simple Encryption And Authentication (AES-GCM) of a UTF8 String
/// using key derived from a password (PBKDF2).
/// </summary>
/// <param name="secretMessage">The secret message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayload">The non secret payload.</param>
/// <returns>
/// Encrypted Message
/// </returns>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// Adds additional non secret payload for key generation parameters.
/// </remarks>
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);
}
 
 
/// <summary>
/// Simple Decryption and Authentication (AES-GCM) of a UTF8 message
/// using a key derived from a password (PBKDF2)
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
/// <returns>
/// Decrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// </remarks>
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 Encoding.UTF8.GetString(plaintext);
}
 
 
/// <summary>
/// Simple Encryption And Authentication (AES-GCM) of a UTF8 string.
/// </summary>
/// <param name="secretMessage">The secret message.</param>
/// <param name="key">The key.</param>
/// <param name="nonSecretPayload">Optional non-secret payload.</param>
/// <returns>Encrypted Message</returns>
/// <remarks>
/// Adds overhead of (Optional-Payload + BlockSize(16) + Message + HMac-Tag(16)) * 1.33 Base64
/// </remarks>
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();
}
}
 
/// <summary>
/// Simple Decryption & Authentication (AES-GCM) of a UTF8 Message
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</param>
/// <param name="key">The key.</param>
/// <param name="nonSecretPayloadLength">Length of the optional non-secret payload.</param>
/// <returns>Decrypted Message</returns>
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;
}
 
}
 
/// <summary>
/// Simple Encryption And Authentication (AES-GCM) of a UTF8 String
/// using key derived from a password.
/// </summary>
/// <param name="secretMessage">The secret message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayload">The non secret payload.</param>
/// <returns>
/// Encrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Must have a password of minimum length;password</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// Adds additional non secret payload for key generation parameters.
/// </remarks>
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);
}
 
/// <summary>
/// Simple Decryption and Authentication of a UTF8 message
/// using a key derived from a password
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
/// <returns>
/// Decrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Must have a password of minimum length;password</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// </remarks>
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);
}
}
}
AESThenHMAC.cs
C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
/*
* 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;
 
/// <summary>
/// Helper that generates a random key on each call.
/// </summary>
/// <returns></returns>
public static byte[] NewKey()
{
var key = new byte[KeyBitSize / 8];
Random.GetBytes(key);
return key;
}
 
/// <summary>
/// Simple Encryption (AES) then Authentication (HMAC) for a UTF8 Message.
/// </summary>
/// <param name="secretMessage">The secret message.</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 Message
/// </returns>
/// <exception cref="System.ArgumentException">Secret Message Required!;secretMessage</exception>
/// <remarks>
/// Adds overhead of (Optional-Payload + BlockSize(16) + Message-Padded-To-Blocksize + HMac-Tag(32)) * 1.33 Base64
/// </remarks>
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);
}
 
/// <summary>
/// Simple Authentication (HMAC) then Decryption (AES) for a secrets UTF8 Message.
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</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 Message
/// </returns>
/// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
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 Encoding.UTF8.GetString(plainText);
}
 
/// <summary>
/// Simple Encryption (AES) then Authentication (HMAC) of a UTF8 message
/// using Keys derived from a Password (PBKDF2).
/// </summary>
/// <param name="secretMessage">The secret message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayload">The non secret payload.</param>
/// <returns>
/// Encrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">password</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// Adds additional non secret payload for key generation parameters.
/// </remarks>
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);
}
 
/// <summary>
/// Simple Authentication (HMAC) and then Descryption (AES) of a UTF8 Message
/// using keys derived from a password (PBKDF2).
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
/// <returns>
/// Decrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Encrypted Message Required!;encryptedMessage</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// </remarks>
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 Encoding.UTF8.GetString(plainText);
}
 
/// <summary>
/// Simple Encryption(AES) then Authentication (HMAC) for a UTF8 Message.
/// </summary>
/// <param name="secretMessage">The secret message.</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 Message
/// </returns>
/// <remarks>
/// Adds overhead of (Optional-Payload + BlockSize(16) + Message-Padded-To-Blocksize + HMac-Tag(32)) * 1.33 Base64
/// </remarks>
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();
}
 
}
 
/// <summary>
/// Simple Authentication (HMAC) then Decryption (AES) for a secrets UTF8 Message.
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</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 Message</returns>
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();
}
}
}
}
 
/// <summary>
/// Simple Encryption (AES) then Authentication (HMAC) of a UTF8 message
/// using Keys derived from a Password (PBKDF2)
/// </summary>
/// <param name="secretMessage">The secret message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayload">The non secret payload.</param>
/// <returns>
/// Encrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Must have a password of minimum length;password</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// Adds additional non secret payload for key generation parameters.
/// </remarks>
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);
}
 
/// <summary>
/// Simple Authentication (HMAC) and then Descryption (AES) of a UTF8 Message
/// using keys derived from a password (PBKDF2).
/// </summary>
/// <param name="encryptedMessage">The encrypted message.</param>
/// <param name="password">The password.</param>
/// <param name="nonSecretPayloadLength">Length of the non secret payload.</param>
/// <returns>
/// Decrypted Message
/// </returns>
/// <exception cref="System.ArgumentException">Must have a password of minimum length;password</exception>
/// <remarks>
/// Significantly less secure than using random binary keys.
/// </remarks>
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);
}
 
}
}

It would be nice to make it into a VS2012 solution which spits out the time and size characteristics. Will bring this "alive". I usually leave my stackoverflow answers in just SO and then forget it. Just wanted to say - "great stuff" !

The AESGCM code doesn't perform any authentication of the associated data (AD part of AEAD). I would have expected the Decrypt to catch it but it seems only the length is used; not sure how decrypt would even be able to check. If you keep the AD length the same Decrypt doesn't complain ... the following piece shows what I'm saying:

    public void Run()
    {
        byte[] key256 = new byte[32];
        for (int i = 0; i < 32; i++)
            key256[i] = Convert.ToByte(i % 256);
        string message = "SuperSecretInfoHere!";
        byte[] nonSecretOrg = Encoding.UTF8.GetBytes("Pay Bob Zero Dollars");
        byte[] nonSecretMod = Encoding.UTF8.GetBytes("Pay Bob $ 1,000,000.");

        // Encrypt with associated data 
        string encrypted = AESGCM.SimpleEncrypt(message, key256, nonSecretOrg);

        // Decrypt with original associated data
        string decrypted = AESGCM.SimpleDecrypt(encrypted, key256, nonSecretOrg.Length);
        Console.WriteLine("Decrypted: {0}", decrypted);
        Console.WriteLine("Auth cleartext: {0}", Encoding.UTF8.GetString(nonSecretOrg));

        // Decrypt with modified associated data (only length checks out)
        string decrypted2 = AESGCM.SimpleDecrypt(encrypted, key256, nonSecretMod.Length);
        Console.WriteLine("Decrypted: {0}", decrypted2);
        Console.WriteLine("Auth cleartext: {0}", Encoding.UTF8.GetString(nonSecretMod));

        Console.ReadLine();
    }

Also, it's best to make the SecureRandom Random = new SecureRandom(); into Random Random = new Random(); since the requirement for GCM isn't a cryptographic random IV like AES-CBC - it's that the IV should be different each time. You could very well use an incremental counter. The reason is speed - I'm seeing a substantial throughput boost (only for small byte sizes, less than 10 bytes) without impacting the security of the scheme. That simulates many but small writes to database cells.

@sidshetye, Sorry, gist, didn't notify me of these comments :-/

In relation to the comment about additional authenticated data, that is prepended to the ciphertext in the clear, ideally meant for headers of data, and it does get checked by the algorithm for modification, it's also used to pass the salt when the password method is used too.

In regards to performance based on the IV generation, since this is single file encryption, i didn't want to use a counter. And while you are right unpredictable isn't necessary, just nonrepeating, I feel better about using SecureRandom than Random, in this example specifically, as the ciphertext (without AD) is ideally indistinguishable from random data in this case.

I've actually made some performance changes to my keyczar-dotnet library though, based on your BouncyBench, but until the next bouncy castle is released on nuget, my library won't be seeing the boost yet.

Thanks a lot, for the comments!

I strongly disagree with @sidshetye's suggestion to use System.Random instead of a CSPRNG. Using System.Random will likely lead to catastrophic failures when you encrypt 100k messages under a key.

The way System.Random is seeded is a 31 bit value returned by GetTickCount(). If we'd assume that this value is totally random, we'd get a nonce collision after on average 46k message. A single reused nonce completely destroys the MAC part of GCM for all future and past message and destroys the confidentiality for that particular message.

But GetTickCount() isn't even random, it's the number of milliseconds since the last reboot. For people who regularly restart their computer, this causes a bias towards small values.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.