Skip to content

Instantly share code, notes, and snippets.

@Krumelur
Created December 7, 2015 14:50
Show Gist options
  • Save Krumelur/836f11e176bcccec96a3 to your computer and use it in GitHub Desktop.
Save Krumelur/836f11e176bcccec96a3 to your computer and use it in GitHub Desktop.
using System.Diagnostics;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace TowerOffense
{
/// <summary>
/// Helpers for cryptography. This class is shared through file linking in:
/// * iOS project
/// * Android project
/// * MS Test project
///
/// 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;
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;
// 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.
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