Tuesday, February 9, 2010

Encrypting User Passwords

The basic idea behind password encryption is that the encryption must be one-way. You should be able to turn plain text into encrypted text, but there should be no way to get back to the plain text only given the encrypted text. This ensures the security of the user's password on your system. To see if the password they've provided matches their original password, you encrypt the password and see if the encrypted versions are the same.

For more on this process, read this excellent post on How To Encrypt User Passwords. The post is great on concepts and rationale, but light on actual code, so I'll present some possible code here.

The basic steps:

1) Generate a string to encrypt by combining the password with a random number

2) Encrypt the string using a digest mechanism (e.g. like we did for the email verification code)

3) Store the encrypted password and the random number in the datastore

#3 should be familiar to you, so I'll provide a sample for 1 and 2. This isn't exactly what the web site I linked talks about, but builds on our previous MD5 hashing code for email verification. In fact, I'm going to retrofit the more secure mechanism back into the email verification code so that I have only one implementation of encrypting in a utility class.

I'll leave out some of the bits that we've already covered with email verification:


//Create the MD5 MessageDigest instance
Copy the code from previous examples here

// Get the random number
SecureRandom random = SecureRandom.getInstance ("SHA1PRNG");
int number = random.nextInt ();

// Build the string to encrypt and normalize it
String passAndNumber = "" + number + password;
passAndNumber = Normalizer.normalize (passAndNumber);

// Encrypt it
Use the MD5 digest and conversion to hex code from previous examples


What you'll end up with is a hex code string suitable for storing in the datastore as an encrypted password. You'll also store the random number you generated.

When the user tries to login, you'll do a similar process to see if they entered the correct password:


// Read the encrypted password and the random number from the data store
String encryptedPass = ....
int number = ....

// Create the MD5 MessageDigest instance
Already shown in email verification example

// Combine the password they entered with the random number from the data store
password = "" + number + password;

// Normalize the password they entered
password = Normalizer.normalize (password);

// Encrypt the password
Use MD5 and conversion to hex code shown before

// Compare
if (code.equals (encryptedPassword))
// Correct password
else
// Incorrect password


Note that I haven't tested this yet. There may very well be details to work out, but they shouldn't be major. The Normalizer class is only available in Java 1.6, so if you're using Java 1.5 you'll need to upgrade.

Also note that to be safe, when you call the String's getBytes method as part of creating the digest, pass in "UTF-8" as the encoding scheme so you know you'll be getting something consistent. This is different from what we did for the email verification code; I recommend refitting the email verification code to use the same mechanism as password encryption. This means you'll need to store two random numbers in the datastore...the one for the password itself, and the one for the email verification code.

4 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. The method signature for normalize() is Normalizer.normalize(CharSequence src, Normalizer.Form form). So "password = Normalizer.normalize (password);" does not compile?

    ReplyDelete
  3. Yes, as noted the above wasn't tested. There's a static form member on Normalizer, try passing that in as the second parameter.

    ReplyDelete