Skip to content

Instantly share code, notes, and snippets.

@Krumelur
Last active November 14, 2015 14:30
Show Gist options
  • Save Krumelur/7a70472ba4a1bd4a2ac0 to your computer and use it in GitHub Desktop.
Save Krumelur/7a70472ba4a1bd4a2ac0 to your computer and use it in GitHub Desktop.
Helper to use managed AES encryption and easily encrypt an decrypt data. To see how the stream based methods work, have a look at `EncryptString()`.
using System.Diagnostics;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace CryptoDemo
{
/// <summary>
/// Helpers for cryptography.
/// Cryptography is unavailable in PCL but on most platforms.
/// </summary>
public static class SharedCryptoHelpers
{
/// <summary>
/// Generates a random encryption key and returns it Base64 encoded.
/// </summary>
/// <returns>The random key as Base64 encoded string.</returns>
public static string GenerateRandomKeyAsBase64()
{
byte[] key = null;
// Aes.Create() will use hardware accelerated encryption if used on Xamarin.iOS!
using (var aesAlgo = Aes.Create())
{
// Must be 128 for Rijndael.
aesAlgo.BlockSize = 128;
// Maximum for Rijndael is 256. This means 14 cycles of repetion when turning plaintext into the final output.
aesAlgo.KeySize = 256;
// Ciphe block chaining mode. This means each encrypted block depends on all previous blocks.
// Hence, an IV (Initialization Vector) is required for the first block.
aesAlgo.Mode = CipherMode.CBC;
// Rijndael works on fixed size blocks but messages come in various lengths. Hence padding is required.
// PKCS7 padding is the most commonly used padding method (see: http://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7).
aesAlgo.Padding = PaddingMode.PKCS7;
aesAlgo.GenerateKey();
key = aesAlgo.Key;
}
return Convert.ToBase64String(key);
}
/// <summary>
/// Encrypts an incoming stream into an outgoing stream.
/// </summary>
/// <param name="streamToWrap">the stream to wrap into a crypto stream</param>
/// <param name="password">the password to use for encryption</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
public static CryptoStream GetEncryptionStream(Stream streamToWrap, string password, int iterations)
{
if (streamToWrap == null)
{
throw new ArgumentException("Cannot encrypt NULL stream!");
}
if (!streamToWrap.CanWrite)
{
throw new ArgumentException("Stream to wrap cannot be written to.");
}
if (String.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("Password must be defined");
}
CryptoStream cryptoStream = null;
// Create a Rijndael algorithm. Note: this way of construction instead of using AesCryptoServiceProvider() is
// better portable across platforms and will call Apple's hardware accelrated CommonCrypto on iOS.
// For details about Rijndael, see Wikipedia: http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
using (var aesAlgo = Aes.Create())
{
// Must be 128 for Rijndael.
aesAlgo.BlockSize = 128;
// Maximum for Rijndael is 256. This means 14 cycles of repetion when turning plaintext into the final output.
aesAlgo.KeySize = 256;
// Cipher block chaining mode. This means each encrypted block depends on all previous blocks.
// Hence, an IV (Initialization Vector) is required for the first block.
aesAlgo.Mode = CipherMode.CBC;
// Rijndael works on fixed size blocks but messages come in various lengths. Hence padding is required.
// PKCS7 padding is the most commonly used padding method (see: http://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7).
aesAlgo.Padding = PaddingMode.PKCS7;
// Set position to start of stream.
streamToWrap.Seek(0, SeekOrigin.Begin);
// Store the salt in the output stream. The salt is not a secret. Salt is used to generate different keys for identical passwords.
var keyInfo = DeriveKey(password, iterations);
streamToWrap.Write(keyInfo.Salt, 0, keyInfo.Salt.Length);
// Store the IV in the output stream. The IV is randomly generated if not set explicitly. It is not a secret and used to create
// different encrypted output for identical plaintext input when using CBC cipher mode.
streamToWrap.Write(aesAlgo.IV, 0, aesAlgo.IV.Length);
// Let the algorithm know our key.
aesAlgo.Key = keyInfo.Key;
// Get an encrypting ICryptoTransform interface from the algorithm.
// Wrap a crypto stream around the plain text stream.
var cryptoTransform = aesAlgo.CreateEncryptor();
cryptoStream = new CryptoStream(streamToWrap, cryptoTransform, CryptoStreamMode.Write);
}
return cryptoStream;
}
/// <summary>
/// Encrypts an incoming stream into an outgoing stream.
/// </summary>
/// <param name="inStream">Unencrypted input stream. Stream will be read from its current position to the end. Be sure to position the stream correctly.</param>
/// <param name="outStream">out stream. Stream has to be writable and seekable. Will be positioned to offset zero.</param>
/// <param name="password">The password to use for encryption. The password is not used directly. A cryptographic strong key will be derived from it.</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
public static void Encrypt(Stream inStream, Stream outStream, string password, int iterations)
{
if (inStream == null)
{
throw new ArgumentException("Cannot encrypt NULL stream!");
}
if (!inStream.CanRead)
{
throw new ArgumentException("Input stream cannot be read from.");
}
if (outStream == null)
{
throw new ArgumentException("Output stream may not be NULL!");
}
if (!outStream.CanWrite)
{
throw new ArgumentException("Output stream cannot be written to.");
}
if (!outStream.CanSeek)
{
throw new ArgumentException("Cannot seek in output stream!");
}
if (String.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("Password must be defined");
}
// Create a crypto stream around the output stream and copy input stream to (encrypted) output.
using(var cryptoStream = SharedCryptoHelpers.GetEncryptionStream(outStream, password, iterations))
{
inStream.CopyTo(cryptoStream);
}
}
/// <summary>
/// Gets a stream that decrypts data.
/// The input stream needs to start with 8 bytes of salt, followed by 16 bytes of IV.
/// </summary>
/// <param name="encryptedInStream">the encrypted stream</param>
/// <param name="password">the password for decryption</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
/// <returns>the decrypted stream</returns>
public static CryptoStream GetDecryptionStream(Stream encryptedInStream, string password, int iterations)
{
if (encryptedInStream == null)
{
throw new ArgumentException("Cannot decrypt NULL stream!");
}
if (!encryptedInStream.CanRead)
{
throw new ArgumentException("Input stream cannot be read from.");
}
if (!encryptedInStream.CanSeek)
{
throw new ArgumentException("Cannot seek in input stream!");
}
CryptoStream cryptoStream = null;
// Create a Rijndael algorithm. Note: this way of construction instead of using AesCryptoServiceProvider() is
// better portable across platforms and will call Apple's hardware accelrated CommonCrypto on iOS.
// For details about Rijndael, see Wikipedia: http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
using (var aesAlgo = Aes.Create())
{
// Must be 128 for Rijndael.
aesAlgo.BlockSize = 128;
// Maximum for Rijndael is 256. This means 14 cycles of repetion when turning plaintext into the final output.
aesAlgo.KeySize = 256;
// Ciphe block chaining mode. This means each encrypted block depends on all previous blocks.
// Hence, an IV (Initialization Vector) is required for the first block.
aesAlgo.Mode = CipherMode.CBC;
// Rijndael works on fixed size blocks but messages come in various lengths. Hence padding is required.
// PKCS7 padding is the most commonly used padding method (see: http://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7).
aesAlgo.Padding = PaddingMode.PKCS7;
// Set position to start of stream to read the IV.
encryptedInStream.Seek(0, SeekOrigin.Begin);
// Read the salt.
byte[] salt = new byte[8];
encryptedInStream.Read(salt, 0, 8);
// Read the IV.
byte[] iv = new byte[16];
encryptedInStream.Read(iv, 0, 16);
aesAlgo.IV = iv;
// Generate the key from the password and the salt.
var keyInfo = DeriveKey(password, salt, iterations);
aesAlgo.Key = keyInfo.Key;
// Get a decrypting ICryptoTransform interface from the algorithm.
var cryptoTransform = aesAlgo.CreateDecryptor();
cryptoStream = new CryptoStream(encryptedInStream, cryptoTransform, CryptoStreamMode.Read);
}
return cryptoStream;
}
/// <summary>
/// Decrypts a stream.
/// The stream needs to start with 8 bytes of salt, followed by 16 bytes of IV.
/// </summary>
/// <param name="encryptedInStream">the encrypted stream</param>
/// <param name="decryptedOutStream">the stream the decrypted output will be written to</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
/// <param name="password">the password for decryption</param>
public static void Decrypt(Stream encryptedInStream, Stream decryptedOutStream, string password, int iterations)
{
if (encryptedInStream == null)
{
throw new ArgumentException("Cannot decrypt NULL stream!");
}
if (!encryptedInStream.CanRead)
{
throw new ArgumentException("Input stream cannot be read from.");
}
if (!encryptedInStream.CanSeek)
{
throw new ArgumentException("Cannot seek in input stream!");
}
if (decryptedOutStream == null)
{
throw new ArgumentException("Output stream may not be NULL!");
}
if (!decryptedOutStream.CanWrite)
{
throw new ArgumentException("Output stream cannot be written to.");
}
using(var cryptoStream = GetDecryptionStream(encryptedInStream, password, iterations))
{
cryptoStream.CopyTo(decryptedOutStream);
}
}
/// <summary>
/// Contains information about an encryption key and the salt value.
/// </summary>
public struct CryptoKeyInfo
{
/// <summary>
/// The key.
/// </summary>
public byte[] Key;
/// <summary>
/// The salt value.
/// </summary>
public byte[] Salt;
}
/// <summary>
/// Uses Rfc2898DeriveBytes (and that corresponds to PBKDF2) to derive a crypto key and a salt from a password.
/// Use this if you want to create a key for first time encryption of data.
/// </summary>
/// <returns>The key information.</returns>
/// <param name="password">Password to use.</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
public static CryptoKeyInfo DeriveKey(string password, int iterations)
{
if (password == null)
{
throw new ArgumentNullException("password", "Password is required!");
}
var key = new CryptoKeyInfo();
// Derive a key together with an 8 byte salt value.
var deriveBytes = new Rfc2898DeriveBytes(password, 8, iterations);
// Get exactly 32 bytes (which maps to 256 bit and is the maximum key size for AES/Rijndael).
key.Key = deriveBytes.GetBytes(32);
key.Salt = deriveBytes.Salt;
return key;
}
/// <summary>
/// Uses Rfc2898DeriveBytes (and that corresponds to PBKDF2) to derive a crypto key from a password and a given salt value.
/// Use this if you want to decrypt data where the salt value is stored as part of the data.
/// </summary>
/// <returns>The key information.</returns>
/// <param name="password">Password to use.</param>
/// <param name="salt">Salt to use to derive the key.</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
public static CryptoKeyInfo DeriveKey(string password, byte[] salt, int iterations)
{
if (password == null)
{
throw new ArgumentNullException("password","Password is required!");
}
if (salt == null || salt.Length != 8)
{
throw new ArgumentException("Salt has to be exactly 8 bytes!");
}
var key = new CryptoKeyInfo();
// Derive a key together with an 8 byte salt value.
var deriveBytes = new Rfc2898DeriveBytes(password, salt, iterations);
// Get exactly 32 bytes (which maps to 256 bit and is the maximum key size for AES/Rijndael).
key.Key = deriveBytes.GetBytes(32);
// The derived salt is the same as the one we pass in.
key.Salt = salt;
return key;
}
/// <summary>
/// Creates the hash of a PIN and returns it as Base64 encoded string.
/// </summary>
/// <param name="pin">PIN to create hash for</param>
public static string CalculatePinHashAsBase64(string pin)
{
string base64Hash = string.Empty;
if (pin == null)
{
pin = string.Empty;
}
string spicedPin = string.Format("Start_SDSClient_{0}_End", pin);
using (var hashAlgo = HashAlgorithm.Create("SHA256"))
{
Debug.Assert(hashAlgo != null, "No hash algorithm returned!");
byte[] hashValue = hashAlgo.ComputeHash(Encoding.UTF8.GetBytes(spicedPin));
base64Hash = Convert.ToBase64String(hashValue);
}
return base64Hash;
}
/// <summary>
/// Encrypts a string and returns it Base64 encoded.
/// </summary>
/// <param name="s">the string to encode</param>
/// <param name="password">The password to use for encryption. The password is not used directly. A cryptographic strong key will be derived from it.</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
/// <returns>the encrypted string</returns>
public static string EncryptString(string s, string password, int iterations)
{
if (s == null)
{
throw new ArgumentException("Cannot encrypt NULL string!");
}
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("Password is required for encryption!");
}
string encryptedString = null;
using (var encryptedOutStream = new MemoryStream())
using (var plainInStream = new MemoryStream(Encoding.UTF8.GetBytes(s)))
{
Encrypt(plainInStream, encryptedOutStream, password, iterations);
encryptedString = Convert.ToBase64String(encryptedOutStream.ToArray());
}
return encryptedString;
}
/// <summary>
/// Decrypts an encrypted string.
/// </summary>
/// <param name="s">the string to decrypt</param>
/// <param name="password">the password to use</param>
/// <param name="iterations">Iterations for hashing algorithm. 10000 is a reasonable value.</param>
/// <returns>the decrypted string. NULL if decryption fails.</returns>
public static string DecryptString(string s, string password, int iterations)
{
if (s == null)
{
throw new ArgumentException("Cannot encrypt NULL string!");
}
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentException("Password is required for encryption!");
}
string decryptedString = null;
try
{
using (var decryptedOutStream = new MemoryStream())
using (var encryptedInStream = new MemoryStream(Convert.FromBase64String(s)))
{
Decrypt(encryptedInStream, decryptedOutStream, password, iterations);
decryptedString = Encoding.UTF8.GetString(decryptedOutStream.ToArray());
}
}
catch (Exception ex)
{
Tracking.Report (ex, true);
// Fail gracefully if a crypto exception occurs.
return null;
}
return decryptedString;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment