![]() |
General Programming »
Algorithms & Recipes »
Encryption
Intermediate
The Art & Science of Storing PasswordsBy gtamirThree approaches to storing passwords - explained with examples. |
C#, VB, SQL, Windows, .NET, Visual Studio, Architect, DBA, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

Almost every website nowadays needs to maintain a list of users and passwords. Many multi-user applications require a way to authenticate users, and passwords seem like a natural.
You don�t necessarily have to provide your own username/password authentication solution. Alternatives include using Active Directory (if you are in a Microsoft domain), Microsoft Passport, LDAP (Lightweight Directory Access Protocol) for non-Microsoft environments, and the membership framework provided in .NET 2.0. All four alternatives have their place, but if you want to understand the logic behind storing passwords and understand how to correctly implement a password management scheme, this article is for you.
This article assumes a web application � slightly different rules apply to a distributed multi-user application. For distributed multi-user applications, the authentication exchange follows slightly different rules.
The simplest approach to manage user names and passwords is to store everything in plaintext (no encryption or scrambling) in a file or database. The result would be something like this:
|
User ID |
User Name |
Password |
|
101 |
Bob |
Snake |
|
102 |
David |
Rainbow6 |
|
103 |
George |
DarkTower |
|
104 |
Eve |
Snake |
SELECT from the table) gains immediate access to all passwords! An employee with legitimate access to the file might print the file or email out the information, and Voila! all the passwords are compromised.
But SQL 2005 supports encryption � isn�t that good enough? No, SQL encryption is not good enough. The built-in encryption only protects the information on disk. If a user is allowed to access the data (perform a SELECT), SQL will automatically decrypt the information. If your web application is allowed to access the data (and how else would you compare the user name and password?) then a hacker hacking your application can access the data as well, gaining the same access as your web application.
SELECTs the information from the remote database. The results of the query are transferred unencrypted over the network. Then, yes, storing passwords �in the clear� is fine.
A better approach for storing passwords (and the only viable alternative if users need to be able to recover passwords) is scrambling the passwords before storing them.
This approach relies on having a secret. The secret is either the scrambling algorithm, or the key used in conjunction with a modern encryption algorithm.
Scrambling (or encrypting) passwords is a reversible operation. The secret is used to garble the password, and the same secret can be used to retrieve the original password. When a user supplies a password, the stored password is de-scrambled using the secret, and the passwords are compared. An alternative approach is to scramble the provided password using the secret and compare the two garbled versions � a match indicates the provided password was good.
If a user needs to retrieve a password, the stored password is de-scrambled and provided to the user (usually via email).
|
User ID |
User Name |
Password |
|
101 |
Bob |
k468dD8F |
|
102 |
David |
56lkV#p6 |
|
103 |
George |
8Fk4lVQ0 |
|
104 |
Eve |
k468dD8F |
If lost password retrieval is a must, then yes, this is the only acceptable solution. A few guidelines though:
A cryptographic hash is also known as a �one-way function�. A hash function takes input of any length, and produces a unique output of constant length. For example, if we hash a password (of any length) with the MD5 cryptographic hash, the result would be a 128 bit number that uniquely corresponds to the password. Cryptographic hashes work on more than passwords � if the cryptographic hash of two files is identical, then the two files are identical as well.
In recent years, as computing power increased, some cryptographic hash functions are no longer recommended for use (MD4, MD5, SHA1). In my opinion, if used just for hashing passwords, you are probably OK. As for me, I modified my code to use SHA2.
When storing hashed passwords, the password is hashed (run through the hashing algorithm) and the resulting hash is stored instead of the password. To compare passwords, just hash the given password using the same hash function and compare the results. If the hashes are the same, the passwords match.
The beauty of a one way function is that there is no way to compute the password based on the hash. Hashed passwords are not immune to brute force attacks � given a dictionary and the password hash, a hacker can compute the hashes of all the words in the dictionary, compare the words with the password hash, and discover the password. This is where strong passwords (containing letters, numbers, and special characters) help us defend against brute-force attacks.
Yes, but please follow these guidelines:
Password length is only needed when encrypting and decrypting the password using a block cipher. Many block ciphers have a block size of 64 bits - anything encrypted will end up with a multiple of 8 bytes for size. Encrypting a 12 byte password would result in a 16 byte output. When the 16 bytes are decrypted, the result would be a 16 byte string with garbage for the last 4 bytes. If the password length is stored, the extra �padding� can be trimmed when decrypting the password. Without the trimming, any na�ve attempts to compare the strings will fail � the 12 character password is not the same as the 16 character decrypted password. Also, attempting to compare the encrypted password to check for a match might fail because the extra 4 bytes of padding might be different from encryption to encryption.
Hashing does not require a password length � the hash of a 12 character password using SHA2 will always result in a 320 bit output, and hashing the same password will always result in the same 320 bit output.
When using a scrambling algorithm such as ROT13 (which I do not recommend), the scrambled password has the same length as the original password. Anyone with access to the password store can then obtain the length of the password. If you decide to use a scrambling algorithm or a stream cipher (as opposed to a block cipher), please make sure to pad the output to hide the password length.
One solution is to store the password length as a column in your table. The problem with this approach is that if a hacker gains access to the information, the length of a password is very useful when attempting a brute-force attack. Knowing that a password has only 5 letters allows the hacker to limit the number of password guesses needed to crack the password.
A better solution is to store the password length as part of the encrypted string. The password length can be pre-pended to the password string (for example, as the first two characters). When the password is decrypted, the first two characters are used to reconstruct the length, and the password can be safely trimmed. Storing the length encrypted with the password makes sure no-one can access the password length without knowing the secret used to encrypt the password.
Storing a message with the length:
// Encode length as first 4 bytes
// Data is in the �message� string
byte[] length = new byte[4];
length[0] = (byte)(message.Length & 0xFF);
length[1] = (byte)((message.Length >> 8) & 0xFF);
length[2] = (byte)((message.Length >> 16) & 0xFF);
length[3] = (byte)((message.Length >> 24) & 0xFF);
csEncrypt.Write(length, 0, 4);
csEncrypt.Write(toEncrypt, 0, toEncrypt.Length);
Retrieving the message from a binary array:
// Holder for the unencrypted message
byte[] fromEncrypt = new byte[encrypted.Length-4];
// Used to convert a byte array to a string
// with a specific encoding
UTF8Encoding textConverter = new UTF8Encoding();
//Read the data out of the crypto stream.
// The first four bytes are the actual length
// The rest is the message + padding
byte[] length = new byte[4];
// Read the data from the decrypted stream
csDecrypt.Read(length, 0, 4);
csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);
int len = (int)length[0] | (length[1] << 8) |
(length[2] << 16) | (length[3] << 24);
//Convert the byte array back into a string.
// Trim the string to the specified length to remove
// the padding
// My default textConverter is UTF8
return textConverter.GetString(fromEncrypt).Substring(0, len);
Hashing (or encrypting) the same password for two users results in the same output. The repeated information can be used maliciously to obtain passwords. If I am a user with access to the password store, I can set my own password to a dictionary word, and then scan the password store for somebody else with the same hashed or encrypted password. If I find a match, I cracked that user�s password.
To prevent the above problem, we can inject some variation into the hashing (or encrypting scheme). For example: if we prefix the password with the user name, the hash or encrypted output will no longer match.
User: Bob
Password: Snake
Hashed password: hash(�Snake�) -> k468dD8F
User: Eve
Password: Snake
Hashed password: hash(�Snake�) -> k468dD8F
Obviously, Bob and Eve have the same password. Even worse, if a hacker obtains our password store, the hacker can pre-compute the hashes for an entire dictionary and look for matches in our password store, greatly accelerating the cracking process.
If we throw the user name into the mix:
User: Bob
Password: Snake
Hashed password: hash(�Bob.Snake�) -> 4Fgja93Q
User: Eve
Password: Snake
Hashed password: hash(�Eve.Snake�) -> k468dD8F
Bob and Eve now have different password hashes. If a hacker gets hold of our password store, the hacker now needs to compute each password hash, specifically for each user. The hacker needs to pre-compute the dictionary hashes with the �Bob.� prefix for Bob and with the �Eve.� prefix for Eve � no free lunch here.
Using a random salt significantly improves the strength of encrypting passwords, and makes brute-force cracking much more costly.
To use a random salt, compute a random number, and use the random number as a component when calculating the hash or when encrypting. Store the random number in a database column so the number can be available later when checking the password.
For example:
// Generate a six-byte salt
public static byte[] GenerateSALT()
{
byte[] data = new byte[6];
new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(data);
return data;
}
When initially storing the password for Bob:
System.Security.Cryptography).
When comparing passwords, follow the same algorithm:
Using a random number is even better than using the user name. User names are not very random - they follow very strict rules. Random numbers (when coming from a good cryptographic random number generator) inject more randomness into the resulting hash or encrypted output. Note that if you decide to use the user name as a salt, the user name can never change. If the user name changes, the hash is no longer valid.
The output from hash functions and encryption algorithms is binary. To store the binary information as textual strings, the information can be encoded.
Two popular encoding schemes are UUENCODE (popular in the Unix world) and Base64 (popular everywhere). Base64 is the encoding scheme used to convert binary attachments for sending over SMTP email.
//Get encrypted array of bytes.
byte[] encrypted = msEncrypt.ToArray();
// Convert to Base64 string
string b64 = Convert.ToBase64String(encrypted);
// Base64 decode
// the data is in the �b64� string
byte[] encrypted = Convert.FromBase64String(b64);
The .NET framework (System.Security.Cryptography) comes with built-in support for several encryption algorithms:
Example - encoding a string:
// Encode the string �message�
// ScrambleKey and ScambleIV are randomly generated
// RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
// rc2.GenerateIV();
// ScrambleIV = rc2.IV;
// rc2.GenerateKey();
// ScrambleKey = rc2.Key
UTF8Encoding textConverter = new UTF8Encoding();
RC2CryptoServiceProvider rc2CSP =
new RC2CryptoServiceProvider();
//Convert the data to a byte array.
byte[] toEncrypt = textConverter.GetBytes(message);
//Get an encryptor.
ICryptoTransform encryptor =
rc2CSP.CreateEncryptor(ScrambleKey, ScrambleIV);
//Encrypt the data.
MemoryStream msEncrypt = new MemoryStream();
CryptoStream csEncrypt = new CryptoStream(msEncrypt,
encryptor, CryptoStreamMode.Write);
//Write all data to the crypto stream and flush it.
// Encode length as first 4 bytes
byte[] length = new byte[4];
length[0] = (byte)(message.Length & 0xFF);
length[1] = (byte)((message.Length >> 8) & 0xFF);
length[2] = (byte)((message.Length >> 16) & 0xFF);
length[3] = (byte)((message.Length >> 24) & 0xFF);
csEncrypt.Write(length, 0, 4);
csEncrypt.Write(toEncrypt, 0, toEncrypt.Length);
csEncrypt.FlushFinalBlock();
//Get encrypted array of bytes.
byte[] encrypted = msEncrypt.ToArray();
Example � decoding a string:
// Decode the �encrypted� byte[]
UTF8Encoding textConverter = new UTF8Encoding();
RC2CryptoServiceProvider rc2CSP = new RC2CryptoServiceProvider();
//Get a decryptor that uses the same key and IV as the encryptor.
ICryptoTransform decryptor =
rc2CSP.CreateDecryptor(ScrambleKey, ScrambleIV);
//Now decrypt the previously encrypted message using the decryptor
// obtained in the above step.
MemoryStream msDecrypt = new MemoryStream(encrypted);
CryptoStream csDecrypt = new CryptoStream(msDecrypt,
decryptor, CryptoStreamMode.Read);
byte[] fromEncrypt = new byte[encrypted.Length-4];
//Read the data out of the crypto stream.
byte[] length = new byte[4];
csDecrypt.Read(length, 0, 4);
csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);
int len = (int)length[0] | (length[1] << 8) |
(length[2] << 16) | (length[3] << 24);
//Convert the byte array back into a string.
return textConverter.GetString(fromEncrypt).Substring(0, len);
The .NET framework (System.Security.Cryptography) comes with built-in support for several cryptographic hashes:
Example � hashing a string:
public byte[] EncryptPassword(string userName, string password,
int encryptionVersion, byte[] salt1, byte[] salt2)
{
string tmpPassword = null;
switch(encryptionVersion)
{
case 2: // password + lots of salt
tmpPassword = Convert.ToBase64String(salt1)
+ Convert.ToBase64String(salt2)
+ userName.ToLower() + password;
break;
case 1: // user name as salt
tmpPassword = userName.ToLower() + password;
break;
case 0: // no salt
default:
tmpPassword = password;
break;
}
//Convert the password string into an Array of bytes.
UTF8Encoding textConverter = new UTF8Encoding();
byte[] passBytes = textConverter.GetBytes(tmpPassword);
//Return the encrypted bytes
if (encryptionVersion == 2)
return new SHA384Managed().ComputeHash(passBytes);
else
return new MD5CryptoServiceProvider().ComputeHash(passBytes);
}
Example � comparing two hashes for equality:
// Just compare to two arrays for equality
// You can add a length comparison, but normally
// all hashes are the same size.
private bool PasswordsMatch(byte[] psswd1, byte[] psswd2)
{
try
{
for(int i = 0; i < psswd1.Length; i++)
{
if(psswd1[i] != psswd2[i])
return false;
}
return true;
}
catch(IndexOutOfRangeException)
{
return false;
}
}
By now, you have enough information to make informed decisions about storing passwords.
The simple guidelines are:
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 21 Jun 2006 Editor: Smitha Vijayan |
Copyright 2006 by gtamir Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |