How to add PBKDF2 password hashing to a Spring Security based project


Although there are some secure password hashing algorithms available, PBKDF2 is not yet implemented in Spring Security. Only BCryptPasswordEncoder, NoOpPasswordEncoder, StandardPasswordEncoder are available in versions 4.0.0.RC1 and 3.2.5.RELEASE.

In this article, we create and use our own PBKDF2 implementation of the PasswordEncoder.

New project using Spring Security

We start by creating a simple Spring Security project using the maven archetype. We install the archetype in our local repository by executing following commands

git clone https://github.com/kolorobot/spring-mvc-quickstart-archetype.git
cd spring-mvc-quickstart-archetype
mvn clean install

Next, we create the project

mvn archetype:generate \
 -DarchetypeGroupId=com.github.spring-mvc-archetypes \
 -DarchetypeArtifactId=spring-mvc-quickstart \
 -DarchetypeVersion=1.0.0 \
 -DgroupId=my.groupid \
 -DartifactId=my-artifactId \
 -Dversion=version

We run the project

mvn test tomcat7:run

And we test it in a browser

http://localhost:8080/

We would like to see the password hash created when the user was stored into a database. We modify our test project to show the user password in the browser. We change method my.groupid.home.HomeController.index()

package my.groupid.home;

import java.security.Principal;
import my.groupid.account.Account;
import my.groupid.account.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {
  @Autowired
  private AccountRepository accountRepository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model, Principal principal) {
    if (principal != null) {
      Account account = accountRepository.findByEmail(principal.getName());
      model.addAttribute("passwordhash", account.getPassword());
    }
    return principal != null ? "home/homeSignedIn" : "home/homeNotSignedIn";
  }
}

and we change src/main/webapp/WEB-INF/views/home/homeSignedIn.html accordingly

<p>
  Welcome to the Spring MVC Quickstart application!
  Your password hash is: <div th:replace="fragments/alert :: alert (type=success, message=${passwordhash})"/>
</p>

Now we test the project. We sign up and we should see our password hash on home screen as it is on the screenshot below.

Password hash created by StandardPasswordEncoder
Password hash created by StandardPasswordEncoder

In the next step we insert new PasswordEncoder into our project. We download Java source code with password hashing routines and save it under my.groupid.config.PasswordHash in our project.

Note: As discussed in Spring Security pull-request (comment#1 and comment#2)), we should change the constants of PasswordHash.java. At least the number of iterations, to make the algorithm run slower (and slow down the potential attacker). For example values

public static final int SALT_BYTE_SIZE = 32;
public static final int HASH_BYTE_SIZE = 32;
public static final int PBKDF2_ITERATIONS = 100000;

cause the routine to take more than 200ms to hash a single password using commodity computer.

We create class my.groupid.config.PBKDF2PasswordEncoder

package my.groupid.config;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import org.springframework.security.crypto.password.PasswordEncoder;

public class PBKDF2PasswordEncoder implements PasswordEncoder {
  @Override
  public String encode(CharSequence cs) {
    try {
      return PasswordHash.createHash(cs.toString());
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException(ex);
    } catch (InvalidKeySpecException ex) {
      throw new RuntimeException(ex);
    }
  }

  @Override
  public boolean matches(CharSequence cs, String string) {
    try {
      return PasswordHash.validatePassword(cs.toString(), string);
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException(ex);
    } catch (InvalidKeySpecException ex) {
      throw new RuntimeException(ex);
    }
  }
}

And we change my.groupid.config. SecurityConfig. passwordEncoder() method to return our new encoder

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new PBKDF2PasswordEncoder();
  }

When testing the project at this stage, we cannot log in using the old account, because the old password hash is in a different format (if we have not cleared the database). However, we can sign up again and see the password hash value.

1000:ca1e8a024192214a321df2a2c98128f3d18a2f46f8dc5175:3ec34431c8cc40a842202ffba3e9395761306614ed030ff3
Password hash generated using PBKDF2PasswordEncoder
Password hash generated using PBKDF2PasswordEncoder

Existing project using Spring Security

We assume that our existing project is using XML Spring configuration and MD5Hex password hashing (commons-codec [DigestUtils](http://commons.apache.org/proper/commons-codec/archives/1.10/apidocs/org/apache/commons/codec/digest/DigestUtils.html).md5Hex(plaintext)). We would like to insert our new PBKDF2PasswordEncoder to the project.

As we have seen, the hash itself has a format

n:salt:hash

All information needed to verify a password hash of single user is stored in a single string. So we do not have to change the database schema of our existing project (add salt and iterations columns to our users table).

We just need to insert our PBKDF2PasswordEncoder to the project and migrate existing password hashes of already signed up users.

Our database is full of hashed passwords in MD5Hex, which we cannot break to obtain plain texts (and re-hash), therefore we fall back to use double hashing:

hash = pbkdf2(md5hex(plaintext), salt)

Usage of double hashing enables us to migrate existing passwords to new scheme. We can alter the users table in a single maintenance operation with deploying the modified project. Since then all new password hashes are created using this new double hashing scheme.

Lets alter our PBKDF2PasswordEncoder to support double hashing

package my.groupid.config;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import org.springframework.security.crypto.password.PasswordEncoder;

public class PBKDF2PasswordEncoder implements PasswordEncoder {
  @Override
  public String encode(CharSequence cs) {
    try {
      return PasswordHash.createHash(DigestUtils.md5Hex(cs.toString()));
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException(ex);
    } catch (InvalidKeySpecException ex) {
      throw new RuntimeException(ex);
    }
  }

  @Override
  public boolean matches(CharSequence cs, String string) {
    try {
      return PasswordHash.validatePassword(DigestUtils.md5Hex(cs.toString()), string);
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException(ex);
    } catch (InvalidKeySpecException ex) {
      throw new RuntimeException(ex);
    }
  }
}

Inject PBKDF2PasswordEncoder by modifying application-context.xml

<beans:bean id="passwordEncoder" class="my.groupid.config.PBKDF2PasswordEncoder"/>

<beans:bean id="userService" class="my.groupid.account.UserService" />

<authentication-manager alias="authenticationManager">
  <authentication-provider user-service-ref="userService">
    <!-- <password-encoder hash="md5"/> -->
    <password-encoder ref="passwordEncoder"/>
  </authentication-provider>
</authentication-manager>

We test the project by singing up and checking the created password hash. We may need to alter the users table to accommodate new longer password hash.

We can migrate existing passwords in the database. We select all users and create appropriate update statements.

PostgreSQL

select 'UPDATE users SET password = ' || users.password || ' WHERE users.id = ' || users.id || ';';

MySQL

select concat('UPDATE users SET password = ', users.password, ' WHERE users.id = ', users.id,';');

The obtained users list is in format

UPDATE users SET password = '47bce5c74f589f4867dbd57e9ca9f808' WHERE users.id = 1;

Let’s save this file, load it in a simple Java program to convert the md5hex_hash values to pbkdf2(md5hex_hash). The result should look like this (password plaintext is ‘aaa’)

UPDATE users SET password = '1000:2ea110f320129bbd18b191b779187de0ade63ea709974bd7:6754cad92f3dee09af38c012f2ed5e44d1863dc031985667' WHERE users.id = 1;

We use this simple Java command line tool to accomplish the conversion

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class Convert {
  public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
    BufferedReader sc = null;
    try {
      sc = new BufferedReader(new InputStreamReader(System.in));
      String line = null;
      do {
        line = sc.readLine();
        if (line != null) {
          Matcher m = Pattern.compile(Pattern.quote("UPDATE users SET password = '") + "(.*)" + Pattern.quote("' WHERE users.id = ") + "(.*);").matcher(line);
          if (m.matches()) {
            System.out.println("UPDATE users SET password = '" + PasswordHash.createHash(m.group(1)) + "' WHERE users.id = " + m.group(2));
          }
        }
      } while (line != null);
    } finally {
      if (sc != null) {
        sc.close();
      }
    }
  }
}

We let the database execute the created script.

Happy hashing.

Note 1: Example implementation is available at github repository.

Note 2: If the existing project uses StandardPasswordEncoder with salt, conversion is a little bit tricky: we have to split the existing hash value into hash and salt, compute StandardPasswordEncoder.digest() and then compute the PBKDF2 with another salt. This also includes, that we have to store two salt values in a password hash string.

Note 3: Migration of existing password hashes is also possible using this SQL routine, but beware, it is much more slower than the Java implementation.