In version 3.2. we used a SHA256 hash of “username+password” (created either on the client (browser) or server) which was stored in the user’s xml file (which is a serialization of the in memory user-object).
Although we never stored the user’s password on disk (in fact in most cases we never even sent it to the server), due to advances in processing power and Credentials Brute Force Attacks, in 2013 that is not a secure way to store password anymore.
After some threads (see TM stores passwords insecurely issue), it was agreed that a solution based on PBKDF2 should be used.
And since we had the requirement to migrate the previous user’s details in to the new 3.3 PBKDF2 format, we made the new algorithm compatible with the previous SHA256 mode.
Here is the formula that we currently use:
- user provides a password in clear text
- we create a SHA256 of the username+password
- we create a 64 bit PBKDF2 of the SHA256 using 20000 interactions using the user’s ID (a GUID) as the salt
passwordHash = PBKDF2 ( SHA256 ( UserName + Password ) , UserId_GUID )
with:
- UserName, UserId_Guid and passwordHash stored in the User’s XML file
- Password never stored (but transmitted over SSL)
- UserID_GUID (the salt) is a .NET GUID which is a 128-bit integer, i.e. 16 bytes (note: although a bit OTT, just to add one more layer of security, in the 3.4 release we will be using RNGCryptoServiceProvider to create it (see Use a stronger random number generator issue and the Is RNGCryptoServiceProvider is 'fast enough' to create a GUID post)
- the PBKDF2 byte size is 64 (which creates a password hash that looks like: 8jesPsP9ExGeoMe/N ezXqh7RWQTdawsUb0znfo6VgD46nRIbAXbcgaPYCRlfLYQK1IeQphESxjZ5EDc/ZD0yFw== (base64 format))
- there are 20000 PBKDF2 interactions
And how was this implemented?
Well, let’s look at the code:
When the user logs in, its username and password are submitted via a WebService method which calls a Login method that calls tmUser.createPasswordHash(password)
...which will call the createPasswordHash (this TMUser tmUser, string password) method
... that first calls the hash_SHA256(this string text, string salt) method with the Username as text and Password as salt (this will calculate the same hash has the one created in 3.2)
... and then feeds that SHA256 hash to the hash_PBKDF2(this string password, Guid salt) method (with the User’s ID GUID used as salt)
The end result is a hash that looks like this when stored in the user’s XML file:
<?xml version="1.0"?>
<TMUser GroupID="1" State="The State" Country="The Country" EMail="TM_alerts@securityinnovation.com" Company="The Company" Title="El Title" LastName="LName" FirstName="FName" UserName="admin" UserID="735868140" ID="d8aac161-0e25-426c-b21c-9cd230be7dba">
<SecretData SessionID="22449f90-b463-4626-9361-6b131276f745" CSRF_Token="1428559804"
PasswordHash="8jesPsP9ExGeoMe/NezXqh7RWQTdawsUb0znfo6VgD46nRIbAXbcgaPYCRlfLYQK1IeQphESxjZ5EDc/ZD0yFw=="/>
<AccountStatus UserEnabled="true" PasswordExpired="false" ExpirationDate="0001-01-01T00:00:00"/>
<Stats LoginFail="2" LoginOk="17" LastLogin="2013-05-22T15:16:18.3987974+01:00" CreationDate="2013-05-16T15:16:22.8624556+01:00"/>
<UserActivities When="130131890354274532" IPAddress="::1" Who="admin" Detail="admin" Action="User Login"/>
Does this protect against brute force attacks?
I believe so.
Because even in the worse case scenario where the entire userdata xml files are compromised (equivalent of an SQL injection that retrieves all user data), the attacker would need to brute force an SHA256 with salt + 20000 interactions of 64 byte PBKDF2 in order to find 1 (one) user's password.
And since each user has unique salts, the attacker would need to do this for each user.
Which is a lot of work/effort, and means that there are probably easier ways for the attacker to get the account details, just like xkcd very cleverly captures with his http://xkcd.com/538/ cartoon:
If you made it this far, you should also be interested in:
- How PBKDF2 interactions affected UnitTests performance by 2.5x
- Testing TeamMentor's password reset feature (now with token stored as a Hash)
- Software Labels – Jeff’s OWASP AppSecDC 2010 presentation (another dropped good idea)
And these amazing posts from the password related blog posts from Troy Hunt:
- Should websites be required to publicly disclose their password storage strategy?
- The only secure password is the one you can’t remember
- Everything you ever wanted to know about building a secure password reset feature
- The science of password selection
- Who’s who of bad password practices – banks, airlines and more
- I’m sorry, but were you actually trying to remember your comical passwords?
- Stronger password hashing in .NET with Microsoft’s universal providers
- Bad passwords are not fun and good entropy is always important: demystifying security fallacies
- I’d like to share my LinkedIn password with you – here’s why
See also this reddit thread about this post (with really good comments and observations)