I have added identity framework to my WebApi and followed the steps outlined here:
http://bitoftech.net/2015/01/21/asp-net-identity-2-with-asp-net-web-api-2-accounts-management/
All of this is working fine.
The problem I have, is that my client has another system of which the API integrates with (to collect data) and that has it's own login methods. So, with that in mind, my client has asked me to use a CustomPasswordHasher to encrypt and decrypt passwords.
What they would like to do is be able to get a password hash and convert it into the actual password so that they can use it to log into the old system (both passwords / accounts will be the same).
I know this is very unothadox but I have no choice in the matter.
My question is, how easy is this to do?
I have found a few topics on how to create a custom password hasher, but none show me how to actually get the password from the hashed password, they only show how to compare.
Currently I have this:
public class PasswordHasher : IPasswordHasher
{
private readonly int _saltSize;
private readonly int _bytesRequired;
private readonly int _iterations;
public PasswordHasher()
{
this._saltSize = 128 / 8;
this._bytesRequired = 32;
this._iterations = 1000;
}
public string HashPassword(string password)
{
// Create our defaults
var array = new byte[1 + this._saltSize + this._bytesRequired];
// Try to hash our password
using (var pbkdf2 = new Rfc2898DeriveBytes(password, this._saltSize, this._iterations))
{
var salt = pbkdf2.Salt;
Buffer.BlockCopy(salt, 0, array, 1, this._saltSize);
var bytes = pbkdf2.GetBytes(this._bytesRequired);
Buffer.BlockCopy(bytes, 0, array, this._saltSize + 1, this._bytesRequired);
}
// Return the password base64 encoded
return Convert.ToBase64String(array);
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
// Throw an error if any of our passwords are null
ThrowIf.ArgumentIsNull(() => hashedPassword, () => providedPassword);
// Get our decoded hash
var decodedHashedPassword = Convert.FromBase64String(hashedPassword);
// If our password length is 0, return an error
if (decodedHashedPassword.Length == 0)
return PasswordVerificationResult.Failed;
var t = decodedHashedPassword[0];
// Do a switch
switch (decodedHashedPassword[0])
{
case 0x00:
return PasswordVerificationResult.Success;
default:
return PasswordVerificationResult.Failed;
}
}
private bool VerifyHashedPassword(byte[] hashedPassword, string password)
{
// If we are not matching the original byte length, then we do not match
if (hashedPassword.Length != 1 + this._saltSize + this._bytesRequired)
return false;
//// Get our salt
//var salt = pbkdf2.Salt;
//Buffer.BlockCopy(salt, 0, array, 1, this._saltSize);
//var bytes = pbkdf2.GetBytes(this._bytesRequired);
//Buffer.BlockCopy(bytes, 0, array, this._saltSize + 1, this._bytesRequired);
return true;
}
}
If I really wanted to I could just do this:
public class PasswordHasher : IPasswordHasher
{
public string HashPassword(string password)
{
// Do no hashing
return password;
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
// Throw an error if any of our passwords are null
ThrowIf.ArgumentIsNull(() => hashedPassword, () => providedPassword);
// Just check if the two values are the same
if (hashedPassword.Equals(providedPassword))
return PasswordVerificationResult.Success;
// Fallback
return PasswordVerificationResult.Failed;
}
}
but that would be crazy, because all the passwords would be stored as plain text. Surely there is a way to "encrypt" the password and "decrypt" it when I make a call?
So, I have tried to be as secure as possible. This is what I have done.
I created a new provider:
public class AdvancedEncryptionStandardProvider
{
// Private properties
private readonly ICryptoTransform _encryptor, _decryptor;
private UTF8Encoding _encoder;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="key">Our shared key</param>
/// <param name="secret">Our secret</param>
public AdvancedEncryptionStandardProvider(string key, string secret)
{
// Create our encoder
this._encoder = new UTF8Encoding();
// Get our bytes
var _key = _encoder.GetBytes(key);
var _secret = _encoder.GetBytes(secret);
// Create our encryptor and decryptor
var managedAlgorithm = new RijndaelManaged();
managedAlgorithm.BlockSize = 128;
managedAlgorithm.KeySize = 128;
this._encryptor = managedAlgorithm.CreateEncryptor(_key, _secret);
this._decryptor = managedAlgorithm.CreateDecryptor(_key, _secret);
}
/// <summary>
/// Encrypt a string
/// </summary>
/// <param name="unencrypted">The un-encrypted string</param>
/// <returns></returns>
public string Encrypt(string unencrypted)
{
return Convert.ToBase64String(Encrypt(this._encoder.GetBytes(unencrypted)));
}
/// <summary>
/// Decrypt a string
/// </summary>
/// <param name="encrypted">The encrypted string</param>
/// <returns></returns>
public string Decrypt(string encrypted)
{
return this._encoder.GetString(Decrypt(Convert.FromBase64String(encrypted)));
}
/// <summary>
/// Encrypt some bytes
/// </summary>
/// <param name="buffer">The bytes to encrypt</param>
/// <returns></returns>
public byte[] Encrypt(byte[] buffer)
{
return Transform(buffer, this._encryptor);
}
/// <summary>
/// Decrypt some bytes
/// </summary>
/// <param name="buffer">The bytes to decrypt</param>
/// <returns></returns>
public byte[] Decrypt(byte[] buffer)
{
return Transform(buffer, this._decryptor);
}
/// <summary>
/// Writes bytes to memory
/// </summary>
/// <param name="buffer">The bytes</param>
/// <param name="transform"></param>
/// <returns></returns>
protected byte[] Transform(byte[] buffer, ICryptoTransform transform)
{
// Create our memory stream
var stream = new MemoryStream();
// Write our bytes to the stream
using (var cs = new CryptoStream(stream, transform, CryptoStreamMode.Write))
{
cs.Write(buffer, 0, buffer.Length);
}
// Retrun the stream as an array
return stream.ToArray();
}
}
Then my PasswordHasher, I changed to this:
public class PasswordHasher : IPasswordHasher
{
// Private properties
private readonly AdvancedEncryptionStandardProvider _provider;
public PasswordHasher(AdvancedEncryptionStandardProvider provider)
{
this._provider = provider;
}
public string HashPassword(string password)
{
// Do no hashing
return this._provider.Encrypt(password);
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
// Throw an error if any of our passwords are null
ThrowIf.ArgumentIsNull(() => hashedPassword, () => providedPassword);
// Just check if the two values are the same
if (hashedPassword.Equals(this.HashPassword(providedPassword)))
return PasswordVerificationResult.Success;
// Fallback
return PasswordVerificationResult.Failed;
}
}
To use this PasswordHasher, you invoke it like this:
var passwordHasher = new PasswordHasher(new AdvancedEncryptionStandardProvider(ConfigurationManager.AppSettings["as:key"], ConfigurationManager.AppSettings["as:secret"]));
This seems to satisfy my conditions. Let me know if there are security risks please!
Related
Based on the documentation, with provided sample data, it should be possible to generate a signed key with value of:
aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404
Here is my code in .Net:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace PlayingWithAmazonS3
{
public class ReadTextFilePerRest
{
private string _regionSample = "us-east-1";
private string _dateSample = "20130524";
private string _secretAccessKeySample = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
private string _canonicalRequestPath = "..\\Files\\SampleFiles\\CanonicalRequest.txt";
private string _stringToSignPath = "..\\Files\\SampleFiles\\StringToSign.txt";
private string _canonicalRequest;
private string _stringToSign;
public void ReadPayloadFiles()
{
_stringToSign = File.ReadAllText(_stringToSignPath);
_canonicalRequest = File.ReadAllText(_canonicalRequestPath);
}
// it needs to return: aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404
public string SigningKey()
{
var keyBytes = Encoding.ASCII.GetBytes("AWS4" + _secretAccessKeySample);
var dateBytes = Encoding.ASCII.GetBytes(_dateSample);
var regionBytes = Encoding.ASCII.GetBytes(_regionSample);
var serviceBytes = Encoding.ASCII.GetBytes("s3");
var requestBytes = Encoding.ASCII.GetBytes("aws4_request");
var stringToSignBytes = Encoding.ASCII.GetBytes(_stringToSign);
using (HMACSHA256 hmac = new HMACSHA256(dateBytes))
{
var dateKey = hmac.ComputeHash(keyBytes);
using (HMACSHA256 hmac2 = new HMACSHA256(regionBytes))
{
var dateRegionKey = hmac2.ComputeHash(dateKey);
using (HMACSHA256 hmac3 = new HMACSHA256(serviceBytes))
{
var dateRegionServiceKey = hmac3.ComputeHash(dateRegionKey);
using (HMACSHA256 hmac4 = new HMACSHA256(requestBytes))
{
var signingKey = hmac4.ComputeHash(dateRegionServiceKey);
using (HMACSHA256 hmac5 = new HMACSHA256(stringToSignBytes))
{
var signature = hmac5.ComputeHash(signingKey);
return ByteToString(signature);
}
}
}
}
}
}
private string ByteToString(IEnumerable<byte> buffer)
{
var sBinary = buffer.Aggregate("", (current, buff) => current + buff.ToString("X2"));
return sBinary;
}
}
}
However, my generated signed key is different. Can anybody tell me where is my mistake?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace PlayingWithAmazonS3
{
/// <summary>
/// this class is only responsible for calculating the final signature for creating a pre-signed Url to
/// communicate through REST with iam service
/// </summary>
public class PreSignedUrlRestSignatureCal
{
private const string RegionSample = "us-east-1";
private const string DateSample = "20150830";
private const string ServiceSample = "iam";
// it is provided as an example by AWS
private const string SecretAccessKeySample = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
/// <summary>
/// this method will be called in main
/// </summary>
public void ExecuteMethod()
{
var finalSignature = SigningKey();
Console.WriteLine("Final Signature: " + finalSignature);
}
private string SigningKey()
{
// generating derived signing key
var derivedSigningKey =
GetSignatureKey(SecretAccessKeySample, DateSample, RegionSample, ServiceSample);
// example signingKey provided by aws for test
var stringToSign = "AWS4-HMAC-SHA256" + "\n" +
"20150830T123600Z" + "\n" +
"20150830/us-east-1/iam/aws4_request" + "\n" +
"f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59";
// generating the final signature
var signature = HmacSha256(stringToSign, derivedSigningKey);
// returning the hex value of the final signature
return ByteToString(signature);
}
/// <summary>
/// calculating hmac-sha256 in .Net
/// </summary>
/// <param name="data"></param>
/// <param name="key"></param>
/// <returns></returns>
private byte[] HmacSha256(string data, byte[] key)
{
const string algorithm = "HmacSHA256";
var kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
/// <summary>
/// get derived signing key (not the final signature) from provided info
/// </summary>
/// <param name="key"></param>
/// <param name="dateStamp"></param>
/// <param name="regionName"></param>
/// <param name="serviceName"></param>
/// <returns></returns>
private byte[] GetSignatureKey(string key, string dateStamp, string regionName, string serviceName)
{
var kSecret = Encoding.UTF8.GetBytes(("AWS4" + key).ToCharArray());
var kDate = HmacSha256(dateStamp, kSecret);
var kRegion = HmacSha256(regionName, kDate);
var kService = HmacSha256(serviceName, kRegion);
var kSigning = HmacSha256("aws4_request", kService);
return kSigning;
}
/// <summary>
/// it returns the hex value of byte[]
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
private string ByteToString(IEnumerable<byte> buffer)
{
var sBinary = buffer.Aggregate("", (current, buff) => current + buff.ToString("X2"));
return sBinary.ToLower();
}
}
}
I've been using the code that was posted here to encrypt and decrypt, written by #nerdybeardo. However, I'm getting the error "pad block corrupted" when trying to decrypt.
The Encryptor class looks like this, which implements encrypt then MAC:
/// <summary>
/// Encrypt/decrypt + HMAC using BouncyCastle (C# Java port)
/// </summary>
/// <typeparam name="TBlockCipher">The type of the block cipher.</typeparam>
/// <typeparam name="TDigest">The type of the digest.</typeparam>
/// <see cref="https://stackoverflow.com/a/13511671/119624"/>
public sealed class Encryptor<TBlockCipher, TDigest>
where TBlockCipher : IBlockCipher, new()
where TDigest : IDigest, new()
{
private readonly Encoding encoding;
private readonly byte[] key;
private IBlockCipher blockCipher;
private BufferedBlockCipher cipher;
private HMac mac;
/// <summary>
/// Initializes a new instance of the <see cref="Encryptor{TBlockCipher, TDigest}"/> class.
/// </summary>
/// <param name="encoding">The encoding.</param>
/// <param name="key">The key.</param>
/// <param name="macKey">The mac key.</param>
public Encryptor(Encoding encoding, byte[] key, byte[] macKey)
{
this.encoding = encoding;
this.key = key;
this.Init(key, macKey, new Pkcs7Padding());
}
/// <summary>
/// Initializes a new instance of the <see cref="Encryptor{TBlockCipher, TDigest}"/> class.
/// </summary>
/// <param name="encoding">The encoding.</param>
/// <param name="key">The key.</param>
/// <param name="macKey">The mac key.</param>
/// <param name="padding">The padding.</param>
public Encryptor(Encoding encoding, byte[] key, byte[] macKey, IBlockCipherPadding padding)
{
this.encoding = encoding;
this.key = key;
this.Init(key, macKey, padding);
}
/// <summary>
/// Encrypts the specified plain.
/// </summary>
/// <param name="plain">The plain.</param>
/// <returns></returns>
public string Encrypt(string plain)
{
return Convert.ToBase64String(EncryptBytes(plain));
}
/// <summary>
/// Encrypts the bytes.
/// </summary>
/// <param name="plain">The plain.</param>
/// <returns></returns>
public byte[] EncryptBytes(string plain)
{
byte[] input = this.encoding.GetBytes(plain);
var iv = this.GenerateInitializationVector();
var cipher = this.BouncyCastleCrypto(true, input, new ParametersWithIV(new KeyParameter(key), iv));
byte[] message = CombineArrays(iv, cipher);
this.mac.Reset();
this.mac.BlockUpdate(message, 0, message.Length);
var digest = new byte[this.mac.GetUnderlyingDigest().GetDigestSize()];
this.mac.DoFinal(digest, 0);
var result = CombineArrays(digest, message);
return result;
}
/// <summary>
/// Decrypts the bytes.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <returns></returns>
/// <exception cref="CryptoException"></exception>
public byte[] DecryptBytes(byte[] bytes)
{
// split the digest into component parts
var digest = new byte[this.mac.GetUnderlyingDigest().GetDigestSize()];
var message = new byte[bytes.Length - digest.Length];
var iv = new byte[this.blockCipher.GetBlockSize()];
var cipher = new byte[message.Length - iv.Length];
Buffer.BlockCopy(bytes, 0, digest, 0, digest.Length);
Buffer.BlockCopy(bytes, digest.Length, message, 0, message.Length);
if (!IsValidHMac(digest, message))
{
throw new CryptoException();
}
Buffer.BlockCopy(message, 0, iv, 0, iv.Length);
Buffer.BlockCopy(message, iv.Length, cipher, 0, cipher.Length);
byte[] result = this.BouncyCastleCrypto(false, cipher, new ParametersWithIV(new KeyParameter(key), iv));
return result;
}
/// <summary>
/// Decrypts the specified bytes.
/// </summary>
/// <param name="bytes">The bytes.</param>
/// <returns></returns>
public string Decrypt(byte[] bytes)
{
return this.encoding.GetString(DecryptBytes(bytes));
}
/// <summary>
/// Decrypts the specified cipher.
/// </summary>
/// <param name="cipher">The cipher.</param>
/// <returns></returns>
public string Decrypt(string cipher)
{
return this.Decrypt(Convert.FromBase64String(cipher));
}
/// <summary>
/// Combines the arrays.
/// </summary>
/// <param name="source1">The source1.</param>
/// <param name="source2">The source2.</param>
/// <returns></returns>
private static byte[] CombineArrays(byte[] source1, byte[] source2)
{
var result = new byte[source1.Length + source2.Length];
Buffer.BlockCopy(source1, 0, result, 0, source1.Length);
Buffer.BlockCopy(source2, 0, result, source1.Length, source2.Length);
return result;
}
/// <summary>
/// Ares the equal.
/// </summary>
/// <param name="digest">The digest.</param>
/// <param name="computed">The computed.</param>
/// <returns></returns>
private static bool AreEqual(byte[] digest, byte[] computed)
{
if (digest.Length != computed.Length)
{
return false;
}
var result = 0;
for (var i = 0; i < digest.Length; i++)
{
result |= digest[i] ^ computed[i];
}
return result == 0;
}
/// <summary>
/// Initializes the specified key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="macKey">The mac key.</param>
/// <param name="padding">The padding.</param>
private void Init(byte[] key, byte[] macKey, IBlockCipherPadding padding)
{
this.blockCipher = new CbcBlockCipher(new TBlockCipher());
this.cipher = new PaddedBufferedBlockCipher(this.blockCipher, padding);
this.mac = new HMac(new TDigest());
this.mac.Init(new KeyParameter(macKey));
}
/// <summary>
/// Determines whether [is valid h mac] [the specified digest].
/// </summary>
/// <param name="digest">The digest.</param>
/// <param name="message">The message.</param>
/// <returns></returns>
private bool IsValidHMac(byte[] digest, byte[] message)
{
this.mac.Reset();
this.mac.BlockUpdate(message, 0, message.Length);
var computed = new byte[this.mac.GetUnderlyingDigest().GetDigestSize()];
this.mac.DoFinal(computed, 0);
return AreEqual(digest, computed);
}
/// <summary>
/// Bouncy Castle Cryptography.
/// </summary>
/// <param name="forEncrypt">if set to <c>true</c> [for encrypt].</param>
/// <param name="input">The input.</param>
/// <param name="parameters">The parameters.</param>
/// <returns></returns>
private byte[] BouncyCastleCrypto(bool forEncrypt, byte[] input, ICipherParameters parameters)
{
try
{
cipher.Init(forEncrypt, parameters);
return this.cipher.DoFinal(input);
}
catch (CryptoException)
{
throw;
}
}
/// <summary>
/// Generates the initialization vector.
/// </summary>
/// <returns></returns>
private byte[] GenerateInitializationVector()
{
using (var provider = new RNGCryptoServiceProvider())
{
// 1st block
var result = new byte[this.blockCipher.GetBlockSize()];
provider.GetBytes(result);
return result;
}
}
}
I have a simple wrapper for the AES engine. It looks like this:
public class AesSha256Encryptor
{
private readonly Encryptor<AesEngine, Sha256Digest> provider;
/// <summary>
/// Initializes a new instance of the <see cref="AesSha256Encryptor"/> class.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="hmacKey">The HMAC key.</param>
public AesSha256Encryptor(byte[] key, byte[] hmacKey)
{
provider = new Encryptor<AesEngine, Sha256Digest>(Encoding.UTF8, key, hmacKey);
}
/// <summary>
/// Encrypts the specified plain.
/// </summary>
/// <param name="plain">The plain.</param>
/// <returns></returns>
public string Encrypt(string plain)
{
return provider.Encrypt(plain);
}
/// <summary>
/// Decrypts the specified cipher.
/// </summary>
/// <param name="cipher">The cipher.</param>
/// <returns></returns>
public string Decrypt(string cipher)
{
return provider.Decrypt(cipher);
}
}
I wanted to be able to have a different salt per database row, so I have a key manager that works like this:
public static class EncryptionKeyManager
{
/// <summary>
/// The salt length limit
/// </summary>
private const int SaltLengthLimit = 32;
/// <summary>
/// Gets the key record.
/// </summary>
/// <returns></returns>
public static KeyRecord GetKeyRecord()
{
// get the shared passphrasefrom appsettings
var sharedPassphrase = GetSharedPassphrase();
// get the client passphrase from config db to sign
var clientPassphrase = GetClientPassphrase();
// generate secure random salt
var salt = GetSalt();
// get both the encryption key and hmac key
// these will be used for Encrypt-then-Mac
var key = GetKeyFromPassphrase(sharedPassphrase, salt);
var hmacKey = GetKeyFromPassphrase(clientPassphrase, salt);
return new KeyRecord
{
SharedKey = key,
HmacKey = hmacKey,
Salt = salt
};
}
/// <summary>
/// Gets the client salt.
/// </summary>
/// <returns></returns>
private static string GetClientPassphrase()
{
var settingsService = ServiceLocator.Current.GetInstance<ISettingService>();
return settingsService.GetSetting(ConstantConfigSettings.EncryptionSettings.ClientPassphrase, defaultValue: "<removed>");
}
/// <summary>
/// Gets the shared passphrase.
/// </summary>
/// <returns></returns>
private static string GetSharedPassphrase()
{
return ConfigurationManager.AppSettings[ConstantConfigSettings.EncryptionSettings.SharedPassphrase] ?? "<removed>";
}
/// <summary>
/// Gets the key from passphrase.
/// </summary>
/// <param name="passphrase">The passphrase.</param>
/// <param name="salt">The salt.</param>
/// <returns></returns>
private static byte[] GetKeyFromPassphrase(string passphrase, string salt)
{
var saltArray = Encoding.UTF8.GetBytes(salt);
var rfcKey = new Rfc2898DeriveBytes(passphrase, saltArray, 10000);
return rfcKey.GetBytes(32); // for a 256-bit key (32*8=128)
}
/// <summary>
/// Gets the salt from a secure random generator..
/// </summary>
/// <param name="maximumSaltLength">Maximum length of the salt.</param>
/// <returns></returns>
private static string GetSalt(int maximumSaltLength = SaltLengthLimit)
{
var salt = new byte[maximumSaltLength];
using (var random = new RNGCryptoServiceProvider())
{
random.GetNonZeroBytes(salt);
}
return Convert.ToBase64String(salt);
}
}
It all gets used like this to encrypt:
// get key and salt from
var keyRecord = EncryptionKeyManager.GetKeyRecord();
var aesSha256Encryptor = new AesSha256Encryptor(keyRecord.SharedKey, keyRecord.HmacKey);
// now encrypt and store, include salt
entity.AccountNumber = aesSha256Encryptor.Encrypt(accountNumber);
entity.SortCode = aesSha256Encryptor.Encrypt(sortCode);
entity.Salt = keyRecord.Salt;
When I want to decrypt, I do the following:
public static class KeyManager
{
/// <summary>
/// Gets the key from passphrase.
/// </summary>
/// <param name="passphrase">The passphrase.</param>
/// <param name="salt">The salt.</param>
/// <returns>A byte array.</returns>
public static byte[] GetKeyFromPassphrase(string passphrase, string salt)
{
var saltArray = Encoding.UTF8.GetBytes(salt);
var rfcKey = new Rfc2898DeriveBytes(passphrase, saltArray, 10000);
return rfcKey.GetBytes(32); // for a 256-bit key (32*8=128)
}
}
var passphraseKey = KeyManager.GetKeyFromPassphrase(this.Passphrase, this.Salt);
var hmacKey = KeyManager.GetKeyFromPassphrase(this.ClientPassphrase, this.Salt);
var aesSha256Encryptor = new AesSha256Encryptor(passphraseKey, hmacKey);
var plaintext = aesSha256Encryptor.Decrypt(this.CipherText);
This is for a SAAS application. My basic idea was to have a passphrase that is core to the SAAS application that is used to encrypt/decrypt, but also have a specific client passphrase that is used to MAC. The reason for this was to spread the keys between endpoints (one in a database and one in a config setting). The salt gets saved to the database so that it can be used to decrypt using the same salt.
Can anyone see what I am doing wrong? Why am I getting the pad block error?
FYI: The passphrases are of the XKCD variety "horse-battery-stapler-correct" style, so they have hyphens in. I'm not sure if that is a red herring though.
I'm also not sure if the unique salt per row is required, or whether I could just hard code the salt? Is that overkill?
Update
For anyone who finds this, the error was simply that the passphrase that I thought was being used was incorrect. The padding error was the result.
It is not clear what code exactly causes your problem (I mean there is no minimal example I could just run and see what is wrong), but I built an example which decrypts correctly without errors, based on your code, so you can look at it and probably spot your error.
I made EncryptionKeyManager.GetSharedPassphrase() public, and it returns fixed string horse-battery-stapler-correct. I made EncryptionKeyManager.GetClientPassphrase() also public and it returns fixed horse-battery.
class Program {
static void Main(string[] args) {
// get key and salt from
var keyRecord = EncryptionKeyManager.GetKeyRecord();
var aesSha256Encryptor = new AesSha256Encryptor(keyRecord.SharedKey, keyRecord.HmacKey);
string targetData = "4343423343";
var encrypted = aesSha256Encryptor.Encrypt(targetData);
var salt = keyRecord.Salt;
var decrypted = Decrypt(encrypted, salt);
Debug.Assert(targetData == decrypted);
Console.WriteLine(decrypted);
Console.ReadKey();
}
private static string Decrypt(string data, string salt) {
var passphraseKey = KeyManager.GetKeyFromPassphrase(EncryptionKeyManager.GetSharedPassphrase(), salt);
var hmacKey = KeyManager.GetKeyFromPassphrase(EncryptionKeyManager.GetClientPassphrase(), salt);
var aesSha256Encryptor = new AesSha256Encryptor(passphraseKey, hmacKey);
var plaintext = aesSha256Encryptor.Decrypt(data);
return plaintext;
}
}
It's a bit disconcerting that you are getting a padding error, that means the mac validated or wasn't checked before decryption. I guess since you are using two different passphrases if you mac passphrase is correct and your encryption is off that could make sense.
The concern with padding errors being leaked are for chosen ciphertext attacks, so just scanning the overly complex class you borrowed it looks like the mac should be checked, however, I'd at least double check if you put in the incorrect mac passphrase that it won't give you a padding error, because if it does that means there is an issue with that class you are using.
Salt per row is the minimum if you are using passphrases, and not overkill. I prefer salt per encryption as you can see in my example.
The other thing you mentioned was spreading the keys by putting one in the database and one in the config. Splitting the storage of the encryption key from the mac key, is not a good way to do this if that is your the goal, but I'm a bit unclear to your goal. If your app was compromised in the database with an sql injection lets say, only the encryption key is needed to decrypt, the mac is there to just validate that the ciphertext hasn't been tampered with. It's much better to encrypt your ciphertext with decrypted keys stored in the database that were decrypted with keys stored in the config, if you just merely want to spread the key storage a little more.
This is the one I currently used
The problem is I am seeing pattern. For example,
/4mbvuxjWEZnXjO/mzjQhAmcA3lGwohhTxwcx3Tvcx5Ax5HThO2KJGKZ9B1luwabKBSuYBwDcFdp4VO1BgIRXTg/yfvt9pZAc45ebUeFQmuI3xcPluwgh9DR+uqekqdO
/4mbvuxjWEZnXjO/mzjQhBOlj2wiF10gXJOFrqmOZ6zeseuSExCrcdDs1pABE+J4WTynWBqO7sCkIXWo0DwnMD4saXqy3hyl7u/cthd2ZNY7bj7m1KbcdMwLVcT9bet9Wj9rJspQAiFAvCHx/NevSQ==
/4mbvuxjWEZnXjO/mzjQhBOlj2wiF10gXJOFrqmOZ6zeseuSExCrcdDs1pABE+J4WTynWBqO7sCkIXWo0DwnME8zI4EUdapiqGlsluJq2Me/R/5P4OV6a/45GwtG2CuRPAM5DVDP8EY5ZflQACXtpA==
All enscrypted URL start with /4mbvuxjWEZnXjO/mzjQhBOlj2wiF10gXJOFrqmOZ6zeseuSExCrcdDs1pABE+J4WTynWBqO7
Well, I want encryption without that pattern.
This is the code I am currently using.
public static string Encrypt(string plainText)
{
string passPhrase = ConfigurationManager.AppSettings["EncryptionKey"];
byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
byte[] keyBytes = password.GetBytes(keysize / 8);
RijndaelManaged symmetricKey = new RijndaelManaged();
symmetricKey.Mode = CipherMode.CBC;
ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);
MemoryStream memoryStream = new MemoryStream();
CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
cryptoStream.FlushFinalBlock();
byte[] cipherTextBytes = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
return Convert.ToBase64String(cipherTextBytes);
}
private const string initVector = "i51K0t4k4yAraY48";
// This constant is used to determine the keysize of the encryption algorithm.
private const int keysize = 128;
public static string Encrypt(string plainText)
{
string passPhrase = ConfigurationManager.AppSettings["EncryptionKey"];
byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
byte[] keyBytes = password.GetBytes(keysize / 8);
RijndaelManaged symmetricKey = new RijndaelManaged();
symmetricKey.Mode = CipherMode.CBC;
ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);
MemoryStream memoryStream = new MemoryStream();
CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
cryptoStream.FlushFinalBlock();
byte[] cipherTextBytes = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
return Convert.ToBase64String(cipherTextBytes);
}
public static string Decrypt(string cipherText)
{
string passPhrase = ConfigurationManager.AppSettings["EncryptionKey"];
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
byte[] keyBytes = password.GetBytes(keysize / 8);
RijndaelManaged symmetricKey = new RijndaelManaged();
symmetricKey.Mode = CipherMode.CBC;
ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, initVectorBytes);
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
byte[] plainTextBytes = new byte[cipherTextBytes.Length];
int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
memoryStream.Close();
cryptoStream.Close();
return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
}
The problem is you are using the same key and the same initialization vector to encrypt very similar strings. This will tend to create similar ciphertext, as you have seen. Vary one or the other.
If you cannot do that (for whatever reason), you could create a few cryptographically random bytes and append them to each URL before encryption and discard them after decryption. Since AES with CBC has a reasonable avalanche effect for subsequent blocks, this should remove the patterns you are observing.
As an aside, I think the code may have some potential issues. You are mixing ASCII and UTF8 encoding between both Encrypt and the single Decrypt functions. You also should be using try/finally or using blocks to ensure the IDisposable implementing classes are cleaned up properly if an exception is thrown.
You can use a very nice library called Cryptsharp. They have many algoritms you can choose from, and they are very easy to use. You can install using nuget
Project page
https://github.com/ChrisMcKee/cryptsharp
the nuget package
http://www.nuget.org/packages/CryptSharp/
Here is my CryptoClass, you can easily call the constructor with password and iVString. After that you have an encryption without pattern if you change at least one value. You can easily manage a routine to use some iVString and avoid to change password.
hope it helps.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace iOSEncryption
{
/// <summary>
/// Represents a simple interface for Symmetric Criptography using Rijndael algorithm
/// </summary>
public class CryptoRijndael
{
public static Byte[] DefaultSalt
{
get
{
return defaultSalt;
}
}
private static Byte[] defaultSalt = new Byte[24]
{
0x6d, 0x61, 0x79, 0x74, 0x68, 0x65, 0x66, 0x6f,
0x72, 0x63, 0x65, 0x62, 0x65, 0x77, 0x69, 0x74,
0x68, 0x79, 0x6f, 0x75, 0x4a, 0x65, 0x64, 0x69
};
private static Byte[] GetRandomArray(Int32 length)
{
Byte[] retVal = new Byte[length];
Random rd = new Random();
for (int i = 0; i < length; i++)
{
retVal[i] = (Byte)rd.Next(0x0, 0xFF);
}
System.Threading.Thread.Sleep(150);
return retVal;
}
/// <summary>
/// Represents an array of Bytes to derives Private Key and IV
/// </summary>
public class Salt
{
private Byte[] _Value = null;
public Byte[] Value
{
get
{
return _Value;
}
}
public Salt()
: this(defaultSalt)
{
}
public Salt(Byte[] value)
{
if (value.Length < 16)
{
throw new InvalidSaltLengthException();
}
else
{
_Value = value;
}
}
public class InvalidSaltLengthException : Exception
{
public InvalidSaltLengthException()
: base("Bytes array must contains at least 16 elements")
{
}
}
}
/// <summary>
/// Represents an array of 32 bytes.
/// A consistent 256 bits private Key
/// </summary>
public class PrivateKey
{
private Byte[] _Value = null;
public Byte[] Value
{
get
{
return _Value;
}
}
public PrivateKey()
: this(GetRandomArray(32))
{
}
public PrivateKey(Byte[] value)
{
if (value.Length != 32)
{
throw new InvalidPrivateKeyLengthException();
}
else
{
_Value = value;
}
}
public class InvalidPrivateKeyLengthException : Exception
{
public InvalidPrivateKeyLengthException()
: base("Bytes array must contains 32 elements")
{
}
}
public class InvalidPrivateKeyException : Exception
{
public InvalidPrivateKeyException()
: base("Private key must be valid and not null")
{
}
}
}
/// <summary>
/// Represents the initializion vector for the simmetric algorithm
/// </summary>
public class InitVector
{
private Byte[] _Value = null;
public Byte[] Value
{
get
{
return _Value;
}
}
public InitVector()
: this(GetRandomArray(16))
{
}
public InitVector(Byte[] value)
{
if (value.Length != 16)
{
throw new InvalidIVLengthException();
}
else
{
_Value = value;
}
}
public class InvalidIVLengthException : Exception
{
public InvalidIVLengthException()
: base("Bytes array must contains 16 elements")
{
}
}
}
public class DecryptException : Exception
{
public DecryptException(Exception innerException) :
base("Decrypt error occurred check for Password, PrivateKey, IV or Salt", innerException)
{
}
}
public class EncryptException : Exception
{
public EncryptException(Exception innerException) :
base("Encrypt error occurred", innerException)
{
}
}
/// <summary>
/// Gets the 256 bits private Key for the simmetric algorithm
/// </summary>
public PrivateKey ActivePrivateKey { get; private set; }
/// <summary>
/// Gets the current array of Bytes for derives Key and IV from a given password
/// </summary>
public Salt ActiveSalt { get; private set; }
/// <summary>
/// Gets the current IV for the simmetric algorithm
/// </summary>
public InitVector ActiveIV { get; private set; }
private Encoding _StringEncodingType = Encoding.GetEncoding(1252);
/// <summary>
/// Gets or Sets the EncodingType. Default is 1252
/// </summary>
public Encoding StringEncodingType
{
get
{
return _StringEncodingType;
}
set
{
_StringEncodingType = value;
}
}
/// <summary>
/// Initializes a new istance of the CryptoRijndael class.\r\n
/// Creates randoms privateKey and IV.
/// </summary>
public CryptoRijndael()
{
ActivePrivateKey = new PrivateKey();
ActiveIV = new InitVector();
}
/// <summary>
/// Initializes a new istance of the CryptoRijndael class.\r\n
/// privateKey must be Byte[32] and initVector must be Byte[16]
/// </summary>
/// <param name="privateKey"></param>
/// <param name="initVector"></param>
public CryptoRijndael(Byte[] privateKey, Byte[] initVector)
{
ActivePrivateKey = new PrivateKey(privateKey);
ActiveIV = new InitVector(initVector);
}
/// <summary>
/// Initializes a new istance of the CryptoRijndael class.\r\n
/// Derives 256bits private Key from password and IV from iVString using Byte[24] default Salt
/// </summary>
/// <param name="password"></param>
public CryptoRijndael(String password, String iVString)
{
ActiveSalt = new Salt(defaultSalt);
Rfc2898DeriveBytes dbKey = new Rfc2898DeriveBytes(password, ActiveSalt.Value);
Rfc2898DeriveBytes dbIV = new Rfc2898DeriveBytes(iVString, ActiveSalt.Value);
ActivePrivateKey = new PrivateKey(dbKey.GetBytes(32));
ActiveIV = new InitVector(dbIV.GetBytes(16));
}
/// <summary>
/// Initializes a new istance of the CryptoRijndael class.\r\n
/// Derives 256bits private Key and IV form password using Byte[24] default Salt
/// </summary>
/// <param name="password"></param>
public CryptoRijndael(String password)
: this(password, defaultSalt)
{
}
/// <summary>
/// Initializes a new istance of the CryptoRijndael class.\r\n
/// Derives 256bits private Key and IV form password using custom Salt
/// </summary>
/// <param name="password"></param>
/// <param name="salt"></param>
public CryptoRijndael(String password, Byte[] salt)
{
ActiveSalt = new Salt(salt);
Rfc2898DeriveBytes db = new Rfc2898DeriveBytes(password, ActiveSalt.Value);
ActivePrivateKey = new PrivateKey(db.GetBytes(32));
ActiveIV = new InitVector(db.GetBytes(16));
}
public void Encrypt(Stream clearData, Stream encryptedData)
{
EncryptException ee = null;
if (ActivePrivateKey == null)
{
throw new PrivateKey.InvalidPrivateKeyException();
}
else
{
Rijndael alg = null;
CryptoStream cs = null;
try
{
alg = Rijndael.Create();
alg.Key = ActivePrivateKey.Value;
alg.IV = ActiveIV.Value;
cs = new CryptoStream(encryptedData, alg.CreateEncryptor(), CryptoStreamMode.Write);
clearData.CopyTo(cs);
cs.Close();
}
catch (Exception ex)
{
ee = new EncryptException(ex);
}
finally
{
if (cs != null)
{
cs.Dispose();
GC.SuppressFinalize(cs);
cs = null;
}
if (alg != null)
{
alg.Dispose();
GC.SuppressFinalize(alg);
alg = null;
}
}
}
if (ee != null)
{
throw ee;
}
}
public void Decrypt(Stream clearData, Stream encryptedData)
{
DecryptException de = null;
if (ActivePrivateKey == null)
{
throw new PrivateKey.InvalidPrivateKeyException();
}
else
{
Rijndael alg = null;
CryptoStream cs = null;
try
{
alg = Rijndael.Create();
alg.Key = ActivePrivateKey.Value;
alg.IV = ActiveIV.Value;
cs = new CryptoStream(clearData, alg.CreateDecryptor(), CryptoStreamMode.Write);
encryptedData.CopyTo(cs);
cs.Close();
}
catch (Exception ex)
{
de = new DecryptException(ex);
}
finally
{
if (cs != null)
{
cs.Dispose();
GC.SuppressFinalize(cs);
cs = null;
}
if (alg != null)
{
alg.Dispose();
GC.SuppressFinalize(alg);
alg = null;
}
}
}
if (de != null)
{
throw de;
}
}
public Byte[] Encrypt(Byte[] clearData)
{
Byte[] encryptedData = null;
MemoryStream encms = new MemoryStream();
MemoryStream clms = new MemoryStream(clearData);
Encrypt(clms, encms);
encryptedData = encms.ToArray();
encms.Close();
encms.Dispose();
GC.SuppressFinalize(encms);
encms = null;
clms.Close();
clms.Dispose();
GC.SuppressFinalize(clms);
clms = null;
return encryptedData;
}
public Byte[] Decrypt(Byte[] encryptedData)
{
Byte[] clearData = null;
MemoryStream encms = new MemoryStream(encryptedData);
MemoryStream clms = new MemoryStream();
Decrypt(clms, encms);
clearData = clms.ToArray();
encms.Close();
encms.Dispose();
GC.SuppressFinalize(encms);
encms = null;
clms.Close();
clms.Dispose();
GC.SuppressFinalize(clms);
clms = null;
return clearData;
}
public String Encrypt(String clearData)
{
return Convert.ToBase64String(Encrypt(StringEncodingType.GetBytes(clearData)));
}
public String Decrypt(String encryptedData)
{
return StringEncodingType.GetString(Decrypt(Convert.FromBase64String(encryptedData)));
}
}
}
I've got a production application that I'm looking to re-build (ground up) on MVC4. Usage of the SimpleMembershipProvider for authentication and authorization seems to be very suitable for my needs, except for one thing: password encryption.
The current production version of the application has a custom MembershipProvider that encrypted passwords and stored them by generating a salt, hashing the password with the salt (SHA256) and then storing the salt as the first X characters of the database-stored password:
MyApp.Security.MyAppMembershipProvider : System.Web.Security.MembershipProvider:
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) {
// ...
u.Email = email.ToLower();
string salt = GenerateSalt();
u.Password = salt + Helper.FormatPassword(salt, password, this.PasswordFormat);
u.FirstName = String.Empty;
u.LastName = String.Empty;
// ...
}
As I convert the application over to MVC4, the obvious issue is that I want my users' old passwords to continue to authenticate them. I'm willing to migrate to a new data schema, but legacy authentication information will need to continue to work.
My question is, is it possible to override the same way with SimpleMembershipProvider? Will I have to use an implementation of ExtendedMembershipProvider? Or, fingers crossed, is there some voodoo easy way I can do this without creating a custom membership provider altogether?
Thanks!
What you are looking for is to implement your own ExtendedMembershipProvider. There doesn't appear to be any way to interfere with the SimpleMembershipProvider's encryption method, so you need to write your own (such as PBKDF2). I chose to store the salt along with the PBKDF2 iterations in the PasswordSalt column of webpages_Membership, and that way you can increase this value later on when computers get faster and upgrade your old passwords on the fly.
Such a template example might look like:
using WebMatrix.Data;
using WebMatrix.WebData;
using SimpleCrypto;
public class CustomAuthenticationProvider : ExtendedMembershipProvider
{
private string applicationName = "CustomAuthenticationProvider";
private string connectionString = "";
private int HashIterations = 10000;
private int SaltSize = 64;
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
try
{
if (config["connectionStringName"] != null)
this.connectionString = ConfigurationManager.ConnectionStrings[config["connectionStringName"]].ConnectionString;
}
catch (Exception ex)
{
throw new Exception(String.Format("Connection string '{0}' was not found.", config["connectionStringName"]));
}
if (config["applicationName"] != null)
this.connectionString = ConfigurationManager.ConnectionStrings[config["applicationName"]].ConnectionString;
base.Initialize(name, config);
}
public override bool ConfirmAccount(string accountConfirmationToken)
{
return true;
}
public override bool ConfirmAccount(string userName, string accountConfirmationToken)
{
return true;
}
public override string CreateAccount(string userName, string password, bool requireConfirmationToken)
{
throw new NotImplementedException();
}
public override string CreateUserAndAccount(string userName, string password, bool requireConfirmation, IDictionary<string, object> values)
{
// Hash the password using our currently configured salt size and hash iterations
PBKDF2 crypto = new PBKDF2();
crypto.HashIterations = HashIterations;
crypto.SaltSize = SaltSize;
string hash = crypto.Compute(password);
string salt = crypto.Salt;
using (SqlConnection con = new SqlConnection(this.connectionString))
{
con.Open();
int userId = 0;
// Create the account in UserProfile
using (SqlCommand sqlCmd = new SqlCommand("INSERT INTO UserProfile (UserName) VALUES(#UserName); SELECT CAST(SCOPE_IDENTITY() AS INT);", con))
{
sqlCmd.Parameters.AddWithValue("UserName", userName);
object ouserId = sqlCmd.ExecuteScalar();
if (ouserId != null)
userId = (int)ouserId;
}
// Create the membership account and associate the password information
using (SqlCommand sqlCmd = new SqlCommand("INSERT INTO webpages_Membership (UserId, CreateDate, Password, PasswordSalt) VALUES(#UserId, GETDATE(), #Password, #PasswordSalt);", con))
{
sqlCmd.Parameters.AddWithValue("UserId", userId);
sqlCmd.Parameters.AddWithValue("Password", hash);
sqlCmd.Parameters.AddWithValue("PasswordSalt", salt);
sqlCmd.ExecuteScalar();
}
con.Close();
}
return "";
}
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
// Hash the password using our currently configured salt size and hash iterations
PBKDF2 crypto = new PBKDF2();
crypto.HashIterations = HashIterations;
crypto.SaltSize = SaltSize;
string oldHash = crypto.Compute(oldPassword);
string salt = crypto.Salt;
string newHash = crypto.Compute(oldPassword);
using (SqlConnection con = new SqlConnection(this.connectionString))
{
con.Open();
con.Close();
}
return true;
}
public override bool ValidateUser(string username, string password)
{
bool validCredentials = false;
bool rehashPasswordNeeded = false;
DataTable userTable = new DataTable();
// Grab the hashed password from the database
using (SqlConnection con = new SqlConnection(this.connectionString))
{
con.Open();
using (SqlCommand sqlCmd = new SqlCommand("SELECT m.Password, m.PasswordSalt FROM webpages_Membership m INNER JOIN UserProfile p ON p.UserId=m.UserId WHERE p.UserName=#UserName;", con))
{
sqlCmd.Parameters.AddWithValue("UserName", username);
using (SqlDataAdapter adapter = new SqlDataAdapter(sqlCmd))
{
adapter.Fill(userTable);
}
}
con.Close();
}
// If a username match was found, check the hashed password against the cleartext one provided
if (userTable.Rows.Count > 0)
{
DataRow row = userTable.Rows[0];
// Hash the cleartext password using the salt and iterations provided in the database
PBKDF2 crypto = new PBKDF2();
string hashedPassword = row["Password"].ToString();
string dbHashedPassword = crypto.Compute(password, row["PasswordSalt"].ToString());
// Check if the hashes match
if (hashedPassword.Equals(dbHashedPassword))
validCredentials = true;
// Check if the salt size or hash iterations is different than the current configuration
if (crypto.SaltSize != this.SaltSize || crypto.HashIterations != this.HashIterations)
rehashPasswordNeeded = true;
}
if (rehashPasswordNeeded)
{
// rehash and update the password in the database to match the new requirements.
// todo: update database with new password
}
return validCredentials;
}
}
And the encryption class as follows (in my case I used a PBKDF2 encryption wrapper called SimpleCrypto):
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace SimpleCrypto
{
/// <summary>
///
/// </summary>
public class PBKDF2 : ICryptoService
{
/// <summary>
/// Initializes a new instance of the <see cref="PBKDF2"/> class.
/// </summary>
public PBKDF2()
{
//Set default salt size and hashiterations
HashIterations = 100000;
SaltSize = 34;
}
/// <summary>
/// Gets or sets the number of iterations the hash will go through
/// </summary>
public int HashIterations
{ get; set; }
/// <summary>
/// Gets or sets the size of salt that will be generated if no Salt was set
/// </summary>
public int SaltSize
{ get; set; }
/// <summary>
/// Gets or sets the plain text to be hashed
/// </summary>
public string PlainText
{ get; set; }
/// <summary>
/// Gets the base 64 encoded string of the hashed PlainText
/// </summary>
public string HashedText
{ get; private set; }
/// <summary>
/// Gets or sets the salt that will be used in computing the HashedText. This contains both Salt and HashIterations.
/// </summary>
public string Salt
{ get; set; }
/// <summary>
/// Compute the hash
/// </summary>
/// <returns>
/// the computed hash: HashedText
/// </returns>
/// <exception cref="System.InvalidOperationException">PlainText cannot be empty</exception>
public string Compute()
{
if (string.IsNullOrEmpty(PlainText)) throw new InvalidOperationException("PlainText cannot be empty");
//if there is no salt, generate one
if (string.IsNullOrEmpty(Salt))
GenerateSalt();
HashedText = calculateHash(HashIterations);
return HashedText;
}
/// <summary>
/// Compute the hash using default generated salt. Will Generate a salt if non was assigned
/// </summary>
/// <param name="textToHash"></param>
/// <returns></returns>
public string Compute(string textToHash)
{
PlainText = textToHash;
//compute the hash
Compute();
return HashedText;
}
/// <summary>
/// Compute the hash that will also generate a salt from parameters
/// </summary>
/// <param name="textToHash">The text to be hashed</param>
/// <param name="saltSize">The size of the salt to be generated</param>
/// <param name="hashIterations"></param>
/// <returns>
/// the computed hash: HashedText
/// </returns>
public string Compute(string textToHash, int saltSize, int hashIterations)
{
PlainText = textToHash;
//generate the salt
GenerateSalt(hashIterations, saltSize);
//compute the hash
Compute();
return HashedText;
}
/// <summary>
/// Compute the hash that will utilize the passed salt
/// </summary>
/// <param name="textToHash">The text to be hashed</param>
/// <param name="salt">The salt to be used in the computation</param>
/// <returns>
/// the computed hash: HashedText
/// </returns>
public string Compute(string textToHash, string salt)
{
PlainText = textToHash;
Salt = salt;
//expand the salt
expandSalt();
Compute();
return HashedText;
}
/// <summary>
/// Generates a salt with default salt size and iterations
/// </summary>
/// <returns>
/// the generated salt
/// </returns>
/// <exception cref="System.InvalidOperationException"></exception>
public string GenerateSalt()
{
if (SaltSize < 1) throw new InvalidOperationException(string.Format("Cannot generate a salt of size {0}, use a value greater than 1, recommended: 16", SaltSize));
var rand = RandomNumberGenerator.Create();
var ret = new byte[SaltSize];
rand.GetBytes(ret);
//assign the generated salt in the format of {iterations}.{salt}
Salt = string.Format("{0}.{1}", HashIterations, Convert.ToBase64String(ret));
return Salt;
}
/// <summary>
/// Generates a salt
/// </summary>
/// <param name="hashIterations">the hash iterations to add to the salt</param>
/// <param name="saltSize">the size of the salt</param>
/// <returns>
/// the generated salt
/// </returns>
public string GenerateSalt(int hashIterations, int saltSize)
{
HashIterations = hashIterations;
SaltSize = saltSize;
return GenerateSalt();
}
/// <summary>
/// Get the time in milliseconds it takes to complete the hash for the iterations
/// </summary>
/// <param name="iteration"></param>
/// <returns></returns>
public int GetElapsedTimeForIteration(int iteration)
{
var sw = new Stopwatch();
sw.Start();
calculateHash(iteration);
return (int)sw.ElapsedMilliseconds;
}
private string calculateHash(int iteration)
{
//convert the salt into a byte array
byte[] saltBytes = Encoding.UTF8.GetBytes(Salt);
using (var pbkdf2 = new Rfc2898DeriveBytes(PlainText, saltBytes, iteration))
{
var key = pbkdf2.GetBytes(64);
return Convert.ToBase64String(key);
}
}
private void expandSalt()
{
try
{
//get the position of the . that splits the string
var i = Salt.IndexOf('.');
//Get the hash iteration from the first index
HashIterations = int.Parse(Salt.Substring(0, i), System.Globalization.NumberStyles.Number);
}
catch (Exception)
{
throw new FormatException("The salt was not in an expected format of {int}.{string}");
}
}
}
}
and it wouldn't be complete without the interface:
public interface ICryptoService
{
/// <summary>
/// Gets or sets the number of iterations the hash will go through
/// </summary>
int HashIterations { get; set; }
/// <summary>
/// Gets or sets the size of salt that will be generated if no Salt was set
/// </summary>
int SaltSize { get; set; }
/// <summary>
/// Gets or sets the plain text to be hashed
/// </summary>
string PlainText { get; set; }
/// <summary>
/// Gets the base 64 encoded string of the hashed PlainText
/// </summary>
string HashedText { get; }
/// <summary>
/// Gets or sets the salt that will be used in computing the HashedText. This contains both Salt and HashIterations.
/// </summary>
string Salt { get; set; }
/// <summary>
/// Compute the hash
/// </summary>
/// <returns>the computed hash: HashedText</returns>
string Compute();
/// <summary>
/// Compute the hash using default generated salt. Will Generate a salt if non was assigned
/// </summary>
/// <param name="textToHash"></param>
/// <returns></returns>
string Compute(string textToHash);
/// <summary>
/// Compute the hash that will also generate a salt from parameters
/// </summary>
/// <param name="textToHash">The text to be hashed</param>
/// <param name="saltSize">The size of the salt to be generated</param>
/// <param name="hashIterations"></param>
/// <returns>the computed hash: HashedText</returns>
string Compute(string textToHash, int saltSize, int hashIterations);
/// <summary>
/// Compute the hash that will utilize the passed salt
/// </summary>
/// <param name="textToHash">The text to be hashed</param>
/// <param name="salt">The salt to be used in the computation</param>
/// <returns>the computed hash: HashedText</returns>
string Compute(string textToHash, string salt);
/// <summary>
/// Generates a salt with default salt size and iterations
/// </summary>
/// <returns>the generated salt</returns>
string GenerateSalt();
/// <summary>
/// Generates a salt
/// </summary>
/// <param name="hashIterations">the hash iterations to add to the salt</param>
/// <param name="saltSize">the size of the salt</param>
/// <returns>the generated salt</returns>
string GenerateSalt(int hashIterations, int saltSize);
/// <summary>
/// Get the time in milliseconds it takes to complete the hash for the iterations
/// </summary>
/// <param name="iteration"></param>
/// <returns></returns>
int GetElapsedTimeForIteration(int iteration);
}
I think I'm going to go a slightly different route after all:
http://pretzelsteelersfan.blogspot.com/2012/11/migrating-legacy-apps-to-new.html
Basically, migrating legacy user data as-is to the UserProfile table and creating a class to validate credentials against the old algorithm if the SimpleMembership validation fails. If legacy validation succeeds, updating password to new algorithm via WebSecurity.ResetToken to modernize it.
Thanks for the help.
I have a two very simple methods to encrypt/decrypt a byte[] using the AesCryptoServiceProvider. But I'm very surprised about the performance of the method.
I tried to encrypt and decrypt as many bytes[] as my computer can and in the first 4 seconds I can encrypt about 2000 times, but in the next 4 seconds, about 1000, in the next four seconds, about 500... at the end i can have about 80 operations per 4 seconds. Why?
Look at the code.
namespace Encrypttest
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Globalization;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("AES provider");
var key = GenerateKey();
DateTime now = DateTime.Now.AddSeconds(2);
var toStop = now.AddSeconds(120);
long operations = 0;
byte[] buffer = new byte[16];
byte[] result = new byte[buffer.Length];
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = 2;
}
for (long i = 0; i < 10000000; i++)
{
result = Encrypt(buffer, key);
Decrypt(result, key);
buffer = result;
operations++;
if (DateTime.Now > now)
{
Console.WriteLine(now.ToLongTimeString() + ";" + operations + ";" + System.Diagnostics.Process.GetCurrentProcess().PrivateMemorySize64);
operations = 0;
now = DateTime.Now.AddSeconds(2);
}
if (toStop < DateTime.Now)
{
break;
}
}
}
/// <summary>
/// Encrypts the specified data.
/// </summary>
/// <param name="data">The data to encrypt</param>
/// <param name="key">The key to encrypt data.</param>
/// <returns>
/// The data encrypted.
/// </returns>
public static byte[] Encrypt(byte[] data, SymmetricKey key)
{
if (data == null || data.Length == 0)
{
throw new ArgumentNullException("data");
}
if (key == null)
{
throw new ArgumentNullException("key");
}
using (AesCryptoServiceProvider providerInLine = new AesCryptoServiceProvider())
{
using (MemoryStream stream = new MemoryStream())
{
using (ICryptoTransform cryptoEncryptor = providerInLine.CreateEncryptor(key.Key, key.IV))
{
using (CryptoStream writerStream = new CryptoStream(stream, cryptoEncryptor, CryptoStreamMode.Write))
{
writerStream.Write(data, 0, data.Length);
writerStream.FlushFinalBlock();
return stream.ToArray();
}
}
}
}
}
/// <summary>
/// Decrypts the specified data.
/// </summary>
/// <param name="data">The data to decrypt</param>
/// <param name="key">The key to decrypt data.</param>
/// <returns>
/// The data encrypted.
/// </returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
public static byte[] Decrypt(byte[] data, SymmetricKey key)
{
if (data == null || data.Length == 0)
{
throw new ArgumentNullException("data");
}
if (key == null)
{
throw new ArgumentNullException("key");
}
using (AesCryptoServiceProvider provider = new AesCryptoServiceProvider())
{
using (MemoryStream memStreamEncryptData = new MemoryStream(data))
{
using (ICryptoTransform cryptoDecryptor = provider.CreateDecryptor(key.Key, key.IV))
{
using (CryptoStream stream = new CryptoStream(memStreamEncryptData, cryptoDecryptor, CryptoStreamMode.Read))
{
byte[] plainTextBytes = new byte[data.Length];
stream.Read(plainTextBytes, 0, plainTextBytes.Length);
return plainTextBytes;
}
}
}
}
}
/// <summary>
/// Generates a random key and initialization vector
/// </summary>
/// <returns>
/// The key and initialization vector.
/// </returns>
public static SymmetricKey GenerateKey()
{
using (AesCryptoServiceProvider provider = new AesCryptoServiceProvider())
{
provider.GenerateKey();
SymmetricKey key = new SymmetricKey(provider.Key, provider.IV);
return key;
}
}
}
public class SymmetricKey
{
/// <summary>
/// The key.
/// </summary>
private byte[] key;
/// <summary>
/// The initialization vector.
/// </summary>
private byte[] iv;
/// <summary>
/// Initializes a new instance of the <see cref="SymmetricKey"/> class.
/// </summary>
public SymmetricKey()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SymmetricKey"/> class.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="iv">The iv.</param>
public SymmetricKey(byte[] key, byte[] iv)
{
this.Init(key, iv);
}
/// <summary>
/// Gets the key.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Byte[] is what the providers need")]
public byte[] Key
{
get { return this.key; }
}
/// <summary>
/// Gets the iv.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Byte[] is what the providers need")]
public byte[] IV
{
get { return this.iv; }
}
/// <summary>
/// Loads the specified key and iv
/// </summary>
/// <param name="newKey">The key.</param>
/// <param name="newIV">The iv.</param>
public void Init(byte[] newKey, byte[] newIV)
{
this.key = newKey;
this.iv = newIV;
}
}
}
The ciphertext is 16 bytes larger than the plaintext due to padding. Since you're using the ciphertext of of one iteration as the plaintext of the next, the length of the buffer grows linearly with the number of iterations so far. This explains the slowdown.
After 2000 iterations it needs to encrypt 30kB per iteration, after 6000 iterations it needs to encrypt 100kB per iteration...