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.
Related
I have this function to read xml values and create an instance of a class.
Is there a way to combine linq and reflection to not specify the properties of the class and create the class with less code lines?
I try to avoid to modify this method if I add or remove some fields to Test_Class
/// <summary>
/// Get the values of a xml node and return a class of type Class_Test
/// </summary>
/// <param name="sXmlFileName">Path to xml file.</param>
/// <param name="sNodeName">Name of node to get values.</param>
/// <returns>Class_Test New class with the values set from XML.</returns>
public static Class_Test Func_ReadXMLNode(string sXmlFileName, string sNodeName)
{
//Load XML
XDocument xml_Document;
Class_Test result;
try
{
xml_Document = XDocument.Load(sXmlFileName);
//Read XML Section
var xmlValues = from r in xml_Document.Descendants(sNodeName)
select new Class_Test
{
sNetworkInterfaceName = Convert.ToString(r.Element("").Value),
iNetworkInterfacePort = Convert.ToInt32(r.Element("").Value),
sCertificateFile = Convert.ToString(r.Element("").Value),
sCertificateName = Convert.ToString(r.Element("").Value),
iKeepAliveSendingTime = Convert.ToInt32(r.Element("").Value),
iMaximumTimeWithoutKeepAlive = Convert.ToInt32(r.Element("").Value),
sSqlServer = Convert.ToString(r.Element("").Value),
sDatabase = Convert.ToString(r.Element("").Value),
iFtpRetries = Convert.ToInt32(r.Element("").Value),
sDetectionFilesDirectory = Convert.ToString(r.Element("").Value),
sImgDirectory = Convert.ToString(r.Element("").Value),
sLocalDirectory = Convert.ToString(r.Element("").Value),
sOffenceDirectory = Convert.ToString(r.Element("").Value),
sTmpDirectory = Convert.ToString(r.Element("").Value)
};
result = xmlValues.FirstOrDefault();
}
catch (IOException Exception1)
{
LogHelper.Func_WriteEventInLogFile(DateTime.Now.ToLocalTime(), enum_EventTypes.Error, "Func_ReadXMLConfig()", "Source = " + Exception1.Source.Replace("'", "''") + ", Message = " + Exception1.Message.Replace("'", "''"));
result = new Class_Test();
}
return result;
}
And this is Class_Text:
/// <summary>
/// Class of operation mode.
/// </summary>
public class Class_OperationMode
{
#region CONFIG_PARAMETERS
/// <summary>
/// Name of the network interface.
/// </summary>
public string sNetworkInterfaceName;
/// <summary>
/// Port of the network interface.
/// </summary>
public int iNetworkInterfacePort;
/// <summary>
/// Path to certificate file.
/// </summary>
public string sCertificateFile;
/// <summary>
/// Name of the certificate.
/// </summary>
public string sCertificateName;
/// <summary>
/// Time to keep alive the connection while sending data.
/// </summary>
public int iKeepAliveSendingTime;
/// <summary>
/// Time before timeout of the connection.
/// </summary>
public int iMaximumTimeWithoutKeepAlive;
/// <summary>
/// Database server instance.
/// </summary>
public string sSqlServer;
/// <summary>
/// Path to .mdf file of database.
/// </summary>
public string sDatabase;
/// <summary>
/// Max retries to try to connect to FTP Server.
/// </summary>
public int iFtpRetries;
/// <summary>
/// Path to detections files directory.
/// </summary>
public string sDetectionFilesDirectory;
/// <summary>
/// Path to images directory.
/// </summary>
public string sImgDirectory;
/// <summary>
/// Path to local directory.
/// </summary>
public string sLocalDirectory;
/// <summary>
/// Path to folder where save and retrieve offences.
/// </summary>
public string sOffenceDirectory;
/// <summary>
/// Path to temp directory.
/// </summary>
public string sTmpDirectory;
#endregion
UPDATE: Thanks to comments, I updated code to:
public static Class_Test Func_ReadXMLNode(string sXmlFileName, string sNodeName){
//Load XML
XDocument xml_Document;
Class_Test result;
try
{
xml_Document = XDocument.Load(sXmlFileName);
//Read XML Section
//Get xml values of descendants
XElement xmlValues = xml_Document.Descendants(sNodeName).FirstOrDefault();
//Create serializer
XmlSerializer serializer = new XmlSerializer(typeof(Class_Test));
//Deserialize
using (XmlReader reader = xmlValues.CreateReader())
{
result = (Class_Test)serializer.Deserialize(reader);
}
}
catch (IOException Exception1)
{
LogHelper.Func_WriteEventInLogFile(DateTime.Now.ToLocalTime(), enum_EventTypes.Error, "Func_ReadXMLConfig()", "Source = " + Exception1.Source.Replace("'", "''") + ", Message = " + Exception1.Message.Replace("'", "''"));
result = new Class_Test();
}
return result;
}
Thanks in advance. Best regards,
JoaquĆn
I think what youre looking for is serialization/deserialization. Theres lots of useful stuff in .net for handling xml serialization. Ive stolen this example from the docs (link underneath).
XmlSerializer serializer = new
XmlSerializer(typeof(OrderedItem));
// A FileStream is needed to read the XML document.
FileStream fs = new FileStream(filename, FileMode.Open);
XmlReader reader = XmlReader.Create(fs);
// Declare an object variable of the type to be deserialized.
OrderedItem i;
// Use the Deserialize method to restore the object's state.
i = (OrderedItem)serializer.Deserialize(reader);
fs.Close();
From https://msdn.microsoft.com/en-us/library/tz8csy73(v=vs.110).aspx
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!
I have some large XML feed files with illegal characters in them (0x1 etc). The files are third-party, I cannot change the process for writing them.
I would like to process these files using an XmlReader, but it blows up on these illegal characters.
I could read the files, filter out the bad characters, save them, then process them... but this is a lot of I/O, and it seems like it should be unnecessary.
What I would like to do is something like this:
using(var origStream = File.OpenRead(fileName))
using(var cleanStream = new CleansedXmlStream(origStream))
using(var streamReader = new StreamReader(cleanStream))
using(var xmlReader = XmlReader.Create(streamReader))
{
//do stuff with reader
}
I tried inheriting from Stream, but when I got to implementing the Read(byte[] buffer, int offset, int count) I lost some confidence. After all, I was planning on removing characters, so it seemed the count would be off, and I'd have to translate each byte to a char which seemed expensive (especially on large files) and I was unclear how this would work with a Unicode encoding, but the answers to my questions were not intuitively obvious.
When googling for "c# stream wrapper" or "c# filter stream" I am not getting satisfactory results. It's possible I'm using the wrong words or describing the wrong concept, so I'm hoping the SO community can square me away.
Using the example above, what would CleansedXmlStream look like?
Here's what my first attempt looked like:
public class CleansedXmlStream : Stream
{
private readonly Stream _baseStream;
public CleansedXmlStream(Stream stream)
{
this._baseStream = stream;
}
public new void Dispose()
{
if (this._baseStream != null)
{
this._baseStream.Dispose();
}
base.Dispose();
}
public override bool CanRead
{
get { return this._baseStream.CanRead; }
}
public override bool CanSeek
{
get { return this._baseStream.CanSeek; }
}
public override bool CanWrite
{
get { return this._baseStream.CanWrite; }
}
public override long Length
{
get { return this._baseStream.Length; }
}
public override long Position
{
get { return this._baseStream.Position; }
set { this._baseStream.Position = value; }
}
public override void Flush()
{
this._baseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
//what does this look like?
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
return this._baseStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
this._baseStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
Inspired by #CharlesMager's comment, I ended up not making a Stream, but rather a StreamReader like so:
public class CleanTextReader : StreamReader
{
private readonly ILog _logger;
public CleanTextReader(Stream stream, ILog logger) : base(stream)
{
this._logger = logger;
}
public CleanTextReader(Stream stream) : this(stream, LogManager.GetLogger<CleanTextReader>())
{
//nothing to do here.
}
/// <summary>
/// Reads a specified maximum of characters from the current stream into a buffer, beginning at the specified index.
/// </summary>
/// <returns>
/// The number of characters that have been read, or 0 if at the end of the stream and no data was read. The number
/// will be less than or equal to the <paramref name="count" /> parameter, depending on whether the data is available
/// within the stream.
/// </returns>
/// <param name="buffer">
/// When this method returns, contains the specified character array with the values between
/// <paramref name="index" /> and (<paramref name="index + count - 1" />) replaced by the characters read from the
/// current source.
/// </param>
/// <param name="index">The index of <paramref name="buffer" /> at which to begin writing. </param>
/// <param name="count">The maximum number of characters to read. </param>
/// <exception cref="T:System.ArgumentException">
/// The buffer length minus <paramref name="index" /> is less than
/// <paramref name="count" />.
/// </exception>
/// <exception cref="T:System.ArgumentNullException"><paramref name="buffer" /> is null. </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="index" /> or <paramref name="count" /> is
/// negative.
/// </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs, such as the stream is closed. </exception>
public override int Read(char[] buffer, int index, int count)
{
try
{
var rVal = base.Read(buffer, index, count);
var filteredBuffer = buffer.Select(x => XmlConvert.IsXmlChar(x) ? x : ' ').ToArray();
Buffer.BlockCopy(filteredBuffer, 0, buffer, 0, count);
return rVal;
}
catch (Exception ex)
{
this._logger.Error("Read(char[], int, int)", ex);
throw;
}
}
/// <summary>
/// Reads a maximum of <paramref name="count" /> characters from the current stream, and writes the data to
/// <paramref name="buffer" />, beginning at <paramref name="index" />.
/// </summary>
/// <returns>
/// The position of the underlying stream is advanced by the number of characters that were read into
/// <paramref name="buffer" />.The number of characters that have been read. The number will be less than or equal to
/// <paramref name="count" />, depending on whether all input characters have been read.
/// </returns>
/// <param name="buffer">
/// When this method returns, this parameter contains the specified character array with the values
/// between <paramref name="index" /> and (<paramref name="index" /> + <paramref name="count" /> -1) replaced by the
/// characters read from the current source.
/// </param>
/// <param name="index">The position in <paramref name="buffer" /> at which to begin writing.</param>
/// <param name="count">The maximum number of characters to read. </param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="buffer" /> is null. </exception>
/// <exception cref="T:System.ArgumentException">
/// The buffer length minus <paramref name="index" /> is less than
/// <paramref name="count" />.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="index" /> or <paramref name="count" /> is
/// negative.
/// </exception>
/// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.IO.TextReader" /> is closed. </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
public override int ReadBlock(char[] buffer, int index, int count)
{
try
{
var rVal = base.ReadBlock(buffer, index, count);
var filteredBuffer = buffer.Select(x => XmlConvert.IsXmlChar(x) ? x : ' ').ToArray();
Buffer.BlockCopy(filteredBuffer, 0, buffer, 0, count);
return rVal;
}
catch (Exception ex)
{
this._logger.Error("ReadBlock(char[], in, int)", ex);
throw;
}
}
/// <summary>
/// Reads the stream from the current position to the end of the stream.
/// </summary>
/// <returns>
/// The rest of the stream as a string, from the current position to the end. If the current position is at the end of
/// the stream, returns an empty string ("").
/// </returns>
/// <exception cref="T:System.OutOfMemoryException">
/// There is insufficient memory to allocate a buffer for the returned
/// string.
/// </exception>
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
public override string ReadToEnd()
{
var chars = new char[4096];
int len;
var sb = new StringBuilder(4096);
while ((len = Read(chars, 0, chars.Length)) != 0)
{
sb.Append(chars, 0, len);
}
return sb.ToString();
}
}
My unit test looks like this:
[TestMethod]
public void CleanTextReaderCleans()
{
//arrange
var originalString = "The quick brown fox jumped over the lazy dog.";
var badChars = new string(new[] {(char) 0x1});
var concatenated = string.Concat(badChars, originalString);
//act
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(concatenated)))
{
using (var reader = new CleanTextReader(stream))
{
var newString = reader.ReadToEnd().Trim();
//assert
Assert.IsTrue(originalString.Equals(newString));
}
}
}
... and usage looks like this:
using(var origStream = File.OpenRead(fileName))
using(var streamReader = new CleanTextReader(origStream))
using(var xmlReader = XmlReader.Create(streamReader))
{
//do stuff with reader
}
If anyone has suggestions for improvements, I'd be happy to hear them.
I tried #JeremyHolovacs stream implementation but it still was not sufficient for my use case:
using (var fstream = File.OpenRead(dlpath))
{
using (var zstream = new GZipStream(fstream, CompressionMode.Decompress))
{
using (var xstream = new CleanTextReader(zstream))
{
var ser = new XmlSerializer(typeof(MyType));
prods = ser.Deserialize(XmlReader.Create(xstream, new XmlReaderSettings() { CheckCharacters = false })) as MyType;
}
}
}
Somehow not all relevant overloads must have been implemented.
I adapted the class as follows and it works as expected:
public class CleanTextReader : StreamReader
{
public CleanTextReader(Stream stream) : base(stream)
{
}
public override int Read()
{
var val = base.Read();
return XmlConvert.IsXmlChar((char)val) ? val : (char)' ';
}
public override int Read(char[] buffer, int index, int count)
{
var ret = base.Read(buffer, index, count);
for (int i=0; i<ret; i++)
{
int idx = index + i;
if (!XmlConvert.IsXmlChar(buffer[idx]))
buffer[idx] = ' ';
}
return ret;
}
public override int ReadBlock(char[] buffer, int index, int count)
{
var ret = base.ReadBlock(buffer, index, count);
for (int i = 0; i < ret; i++)
{
int idx = index + i;
if (!XmlConvert.IsXmlChar(buffer[idx]))
buffer[idx] = ' ';
}
return ret;
}
}
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...