6

I have a Spring boot 2.0.1 service to which I added Basic authentication which uses BCrypt for hashing. But this service which used to give an average of 400 ms before adding Basic auth is now taking more than 1 second. I am using User details service which looks up the sent user name in a hash map and returns UserDetails. I tried reducing BCrypt rounds down to 4 but that didn't make much of a difference.

Earlier I had stateless authentication enabled which I later disabled but again performance stayed bad. This service is hosted in a Docker container.

Below is my Security config.

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;

    @Autowired
    public SecurityConfig(UserDetailsServiceImpl service) {
        this.userDetailsService = service;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        Map encoders = new HashMap<>();
        encoders.put(BCRYPT_ID, new BCryptPasswordEncoder(BCRYPT_ROUNDS));
        return new DelegatingPasswordEncoder(BCRYPT_ID,encoders);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors()
            .and()
            .csrf().disable()
            .httpBasic();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
}

Please let me know if I am missing something.

Update: I ran benchmarks and it looks like BCrypt encoder is making the application slow. I found some Stack Overflow answers discussing that BCrypt hash calculation is a blocking call.

About hardware: The service host machine has Intel Xeon E5, 16 GB memory. It hosts 4 Spring boot Services each assigned 2 GB running inside a Docker container.

senjin.hajrulahovic
  • 2,961
  • 2
  • 17
  • 32
Ketan
  • 373
  • 1
  • 6
  • 18
  • Have you benchmarked your application? Do you know _where_ it is slow? Saying "I have a very complicated black box. And I am running it inside a another very complicated bloack box. And I am running that inside another very complicated black box." is not helpful to for people to provide decent answers. Just look at the [one provided](https://stackoverflow.com/a/51800271/2071828) - it's basically an unsubstatiated guess. – Boris the Spider Aug 11 '18 at 13:55
  • Agreed. I ran the benchmarks and have updated the question. – Ketan Aug 11 '18 at 15:42
  • 2
    BCrypt is **supposed to be slow**. So if it's BCrypt that is slowing things down, then that is intended behaviour. – Boris the Spider Aug 11 '18 at 15:44
  • I came across this SO question: https://stackoverflow.com/questions/36471723/bcrypt-performance-deterioration Do you think this is an expected behavior or a Spring security BCrypt implementation issue? – Ketan Aug 11 '18 at 16:12
  • @BoristheSpider Other thing, I tried SCrypt encoder in place of BCrypt and it gives average performance of 300 ms compared to 1000 ms for BCrypt. From what I have read till now, SCrypt, although recent, is a good hashing algorithm. So just wondering is SCrypt designed to give good performance along with security? – Ketan Aug 11 '18 at 17:07
  • No. NO. **NO**. The whole point of a password hashing algorithm to is to be **slow**. As slow as possible. Unavoidably slow. Unfixably slow. This is the very reason to use a password hashing algorithm - so that if your user database is lost, then the attackers will not be able to brute force the passwords because the hashing algorithm is **so slow**. SCrypt is designed to be both **slow** and **memory intensive** - to protect against GPU based brute forcing. – Boris the Spider Aug 11 '18 at 17:12
  • You need to set the difficulty of the algorithm you use to the **maximum possible slowness** - because the slower it is the more **secure your password hashes are**. Determine the maximum possible tolerance for slowness, and set the parameters accordingly. – Boris the Spider Aug 11 '18 at 17:14
  • Please please please read [this post](https://security.stackexchange.com/a/31846/59196). – Boris the Spider Aug 11 '18 at 17:16
  • @BoristheSpider Thanks for the link, very informative. I understand that hashing algorithms are designed to be very slow which makes them secure. I guess now my question is, if SCrypt too like BCrypt designed to be slow, why does it give better performance on our hardware set up than BCrypt? I have run benchmarks with BCrypt log rounds 4 (minimum allowed) but it is still considerably slower than SCrypt. So what to make of it? – Ketan Aug 11 '18 at 17:31

4 Answers4

6

So after searching lot about BCrypt encoding and decoding , I have finally found the solution to maintain the performance of spring boot project without major delays caused by BCrypt encoding and decoding.

So BCrypt hashing algorithm works on certain rounds. More the number of rounds you use in your BCrypt encoding, more the space and memory your project will consume to do the encoding as well as decoding (password).

Having said that, I would also like to mention, more the number of rounds you use while encoding your password, more secure it will be.

Most of us would have used these below lines of code to generate the bycrypt credentials in our code

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); // here no of rounds is 16
String result = encoder.encode("password");
system.out.println("encoded password" + result );

The number of rounds that this java code used for generating the BCrypt encoded password is 16 which is too high. the standard round number that can help to serve the balance between time, memory and security is 10.

so if we have to change the number of rounds in the BCrypt Encoding below is what you need to set

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);

When I used 16 rounds, it took me 6 sec to hit the service with basic authentication (3 to 4 sec on average for encoding and decoding BCrypt password)

Where as when I used 10 rounds, I was on able to hit the service with in an average time between 1 and 1.5 sec

There is no standard no of rounds that you should opt for. You should use the maximum number of rounds which is tolerable, performance-wise, in your application. The number of rounds is a slowdown factor, which you use on the basis that under normal usage conditions, such a slowdown has negligible impact for you (the user will not see it, the extra CPU cost does not imply buying a bigger server, and so on). This heavily depends on the operational context: what machines are involved, how many user authentications per second... so there is no one-size-fits-all response.

Markus
  • 2,071
  • 4
  • 22
  • 44
Divya J
  • 455
  • 5
  • 8
5

Some time ago I had the same issue with a server while working on a project for my customer. I analysed the service with jvisualvm. The results were really clear:

bcrypt jvisualvm sampling

I used jmeter to fire 5000 requests against my service's api. I also restarted the service for every measurement and always run a 5000 samples to warm up the JIT compiler before every measurement.

My results were this:

Samples     |    avg [ms]|    min [ms]|    max [ms]| throughput [requests/s]
------------+------------+------------+------------+------------------------
    5000    |         125|          71|         363|                    78.1    
------------+------------+------------+------------+------------------------

The service I analyses was a very simple data access service that just provided access to a database through a REST interface. Therefore I could find out how big the impact of bcrypt was with my jmeter tests.

I then run multiple jmeter tests with different password encoders like MD5, SHA-256, bcrypt, scrypt and pbkdf2.

Here are some of my measurements that might help you make a decision or for further investigation:

algorithm   |    avg [ms]|    min [ms]|    max [ms]| throughput [requests/s]
------------+------------+------------+------------+------------------------
MD5         |           5|           1|          61|                    1443    
------------+------------+------------+------------+------------------------
SHA-256     |           5|           2|          34|                    1464    
------------+------------+------------+------------+------------------------
bcrypt      |         125|          71|         363|                    78.1    
------------+------------+------------+------------+------------------------
scrypt      |         122|          54|        1232|                    79.2    
------------+------------+------------+------------+------------------------
pbkdf2      |         833|         421|        1606|                      12    
------------+------------+------------+------------+------------------------

The service that I analysed is not exposed to the internet. It runs in a safe network zone and performance was more critical then security. So we decided to use a SHA-256 instead of bcrypt.

I think that:

  1. if performance and security is important: choose the most secure algorithm and design your services in a way they can be scaled by adding more machines.
  2. if performance is more important then security: choose the algorithm that is fast enough for your requirements. In my case SHA-256.

But you should always try to achieve 1.

The measurements I showed should be interpreted relativ and not be seen as absolut values.

PS: I know it would be better to execute performance tests that test the encryption api in isolation instead of testing them through a service's REST interface, but I didn't had time yet to setup that kind of test. Hopefully I will have more time to investigate it in the future and if so I will come back update this answer.

René Link
  • 48,224
  • 13
  • 108
  • 140
3

You are creating a BCryptPasswordEncoder instance without passing a SecureRandom. So every time when you encode your password BCrypt will create a new instance of SecureRandom (this is quite an CPU instense operation and is requried for generating the salt). You can check the BCrypt.class source code.

public static String gensalt(int log_rounds) {
    return gensalt(log_rounds, new SecureRandom());
}

public static String gensalt() {
    return gensalt(10);
}

public static String gensalt(int log_rounds, SecureRandom random) {
    ...
}

And BCryptPasswordEncoder.class

    if (this.random != null) {
        salt = BCrypt.gensalt(this.strength, this.random);
    } else {
        salt = BCrypt.gensalt(this.strength);
    }

So use the public BCryptPasswordEncoder(int strength, SecureRandom random) constructor, but remember that creating the SecureRandom instance every time is more safe than using the same instance all the time.

  • Not sure what are the good practices around BCrypt usage. All the documentation I have seen till now doesn’t explicitly provide Secure Random. – Ketan Aug 11 '18 at 16:25
  • @Ketan this suggestion is likely to make very little difference, but it won't affect the security of BCrypt so feel free to try it. – Boris the Spider Aug 11 '18 at 16:43
  • 2
    As a additional note. A secure hash algorithm has to be slow, because this slowness prevents brute force attacks. – user743414 Sep 11 '18 at 20:59
1

A slower hash function does not greatly impact the usability but provides better protection against brute force attacks.

https://security.stackexchange.com/questions/150620/what-is-the-purpose-of-slowing-down-the-calculation-of-a-password-hash

senjin.hajrulahovic
  • 2,961
  • 2
  • 17
  • 32