Blogs
Understanding Liferay's Hashing algorithms and how their performance affects logins.

Introduction
Recently there have been a bunch of questions lately in Slack, in Ask, and even in Liferay Support tickets complaining about the time it takes to log into Liferay and what can be done to improve performance, specifically targeting the hash algorithms...
TL;DR - Liferay has
increased the rounds on the PBKDF2 hash which is detrimental to your
login performance. Set the
passwords.encryption.algorithm=PBKDF2WithHmacSHA1/160/128000
in your portal-ext.properties, force users to change passwords (notes
far below) and consider changing to a BouncyCastle-based implementation.
Now, if you grab a copy of the Liferay DXP Performance Benchmark Study, you can read really impressive statements like:
At 45,750 virtual users, we exceed the established performance budget of this test (i.e., sub 1 second login times).Thus, the performance inflection point for login is roughly between 45,500 and 45,750 virtual users while stable performance and throughput is around 47,000 virtual users.
These numbers sound fantastic, right? Too good to be true maybe?
Well, they are true, I can tell you, but there's a catch - their choice of encryption (hashing) algorithm.
Liferay Password Encryption Algorithms
Although they're referred to as encryption algorithms, they're actually 1-way hashing algorithms.
Liferay does not encrypt passwords. Encrypting a value means that you use a password or key to encrypt the data, but that you can also use that password or key to decrypt back to the original value. That would be a bad practice for passwords, because Liferay somewhere would have to have access to the password or key used to do the encryption, so it would be in a format for a hacker to steal and use to decrypt all of your passwords. So Liferay does not encrypt your passwords...
Instead, when you're creating your account and set your
password, Password123
, the algorithm processes this as
a 1-way hash, meaning it goes from Password123 ->
Fa94R8pB...
, but it can never go from Fa94R8pB... ->
Password123
. The Fa94R8pB...
value is then
stored in the database as the "encrypted password" for
your account.
So each time you log in, Liferay calculates the hash for the password you've provided and then that hash is compared against the value that is stored in the database.
Password123
becomes Fa94R8pB...
and
if that matches what is stored in the database, then login is
successful. If it doesn't match, the login fails and you get to try again.
This is important in case a hacker gets your database or
specifically your User_
table. Since the hashes are all
1-way, they can't decrypt Fa94R8pB...
to get
your original password, however they can try and use brute force to
find a password that hashes to Fa94R8pB...
. Depending
upon the hashing algorithm being used in Liferay and whether your
users are, in fact, using simple passwords like
Password123
, hardware systems these days can quickly
identify some passwords and the hacker can then worm their way in.
Additionally some hashing algorithms are kind of weak in that other different words can hash to the same value. This is referred to as a "collision". For example, using MD5 as the hashing algorithm, here's an example of a collision:
Input 1: abcdefghijklmnopqrstuvwxyz MD5 Output 1: c3fcd3d76192e4007dfb496cca67e13b Input 2: abcdefghijklmnopqrstuvwxyy MD5 Output 2: c3fcd3d76192e4007dfb496cca67e13b
So if my password was input 1, the MD5 hash in
the database would be the c3fcd...
value. Now, maybe my
password is really complex so I feel safe, but a hacker who can brute
force the algorithm can perhaps find another password (by happenstance
or perhaps even some underground tools) to find a string that collides
with (or generates the same hash code as) the c3fcd...
hash. Since Liferay is just comparing the calculated hash values, they
can enter for example input 2 and Liferay would treat that as a
successful login even though the password is not the same.
So there has been a long-running battle with hackers and hash algorithm complexity. From a security perspective, you want an algorithm that generates a (hopefully) unique value that does not have many collisions and also one that takes time to protect against brute force attacks. As hardware improvements make it easier to find passwords or collisions using brute force, the hashing algorithms have had to add complexity to make them harder to attack.
The only problem for us, as the hashing algorithms get more complex, they typically always get slower...
Liferay currently supports nine different hashing algorithms:
- BCRYPT
- MD2
- MD5
- PBKDF2WithHmacSHA1
- SHA
- SHA-256
- SHA-384
- SSHA
- UFC-CRYPT
Some of these algorithms are configurable, but they each have different performance characteristics and complexity.
Oh, and the hashing algorithm used in the performance study I
wrote of above? Those numbers are achieved using the hashing
algorithm NONE - yeah, that's right, there's no
hashing at all in order to hit those numbers. In their database,
your Password123
is stored right there in the
User_
table as Password123
. So
NONE clearly offers great performance, but if a
hacker gets your User_
table, they have all of your
accounts and can easily violate you.
So, in my personal opinion, the login metrics from the
performance study are worthless since they do not represent a real
world scenario. Even for an intranent-only implementation where the
only users are direct employees, all it takes is one disgruntled
employee who can read the User_
table and your
passwords are exposed.
Liferay Default Encryption Algorithm Changes
So for those that don't know, Liferay's default encryption
algorithm in 7.x is PBKDF2WithHmacSHA1
, but even that has
slightly changed over time.
You'll typically find it defined in the
portal.properties
file such
as PBKDF2WithHmacSHA1/160/720000
where the first number
after the slash is the key size and the second number is the number of rounds.
In Liferay, the number of rounds has been increasing over time; it used to be 128,000, and some time it got bumped to 720,000, but even now Liferay is behind OWASP's current recommendation of 1,300,000 rounds (which will impact performance even farther) and Liferay is preparing to update the default to match.
These changes are appropriate from a security standpoint, but they also significantly impact performance.
Raw Performance Info
Before you think about changing the hashing algorithm, one of the two things you need to know is the performance impact of the algorithm. We don't really publish any stats on the supported algorithms, plus there's the issue that performance of the CPU-bound algorithms will depend upon the system you run on...
So to help get some performance info that you can use to test on your hardware, I've created a simple set of Gogo commands. You'll find them in the Github repository.
The basic syntax is to use hash:algorithm password
hashes threads
where algorithm comes from
the table below, password is the password to hash,
hashes is the number of hashes to complete and
threads is the number of threads invoking the hashing algorithm.
Why is threads important? Well, consider if you have 100 people trying to log in at the same time, that's going to be 100 hashes that need to occur, but they'll all be on separate threads. So the implementation takes a number of hashes to complete and the number of threads to use to complete the hashes.
Take care when you run this because it will churn your system quite a bit. The first time I tried with 10,000 hashes and 200 threads and my system was tied up for a long, long time.
Here's the complete table of commands:
Algorithm | Command | Details |
---|---|---|
NONE | hash:none password hashes
threads | This does no hashing at all, it shows how the performance study paper gets away with its numbers. |
BCRYPT | hash:bcrypt password
rounds hashes threads |
Rounds here is the number of internal rounds
the hash algorithm will hash. From the
portal.properties example, this is akin to using
BCRYPT/10 for example if you pass 10 for the
rounds. |
MD2 | hash:md2 password hashes threads
| Uses the MD2 hash. |
MD5 | hash:md5 password hashes
threads | Uses the MD5 hash. |
PBKDF2 | hash:pbkdf2 password keySize rounds
hashes threads | This
uses the PBKDF2WithHmacSHA1 which is the
default for Liferay. Specify the keySize to use
(Liferay uses 160) and the number of rounds (Liferay
currently recommends 720,000 but soon will increase to
1,300,000). Using key size of 160 and rounds of 128,000 is the
equivalent in the portal.properties as
PBKDF2WithHmacSHA1/160/128000 . |
hash:simplepbkdf2 password hashes
threads | This is a
manufactured method, it actually does three different
runs using PBKDF2WithHmacSHA1/160/128000 ,
PBKDF2WithHmacSHA1/160/720000 , and
PBKDF2WithHmacSHA1/160/1300000 so you can compare
the changes. | |
SHA | hash:sha password hashes threads
| Uses the SHA hash. |
SHA-256 | hash:sha256 password
hashes threads | Uses the SHA-256 hash. |
SHA-384 | hash:sha384 password hashes threads
| Uses the SHA-384 hash. |
SSHA | hash:ssha password
hashes threads | Uses the SSHA hash. |
UFC-CRYPT | hash:ufccrypt password hashes threads
| Uses the UFC-Crypt hash. |
hash:all password hashes
threads | Another manufactured method, this one runs all of the hash methods using the given password, hashes and threads, allows you to compare them all. |
Now really the importance of the numbers you get here will really depend upon the hardware you're hosting Liferay on.
On my Intel-based iMac, 3.8 GHz 8-Core Intel Core i7, using a password of Password123 and 1,000 hashes and 20 threads:
g! hash:all Password123 1000 20 NONE: Hashed Password: {NONE}Password123 Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms BCRYPT/10: Hashed Password: {BCRYPT}$2a$10$6nBoaq5HOcUv.zMK0x4C0ueDXYMEPK283.LJ3OomZ9twD4I HSLYFi Time for 1000 on 20 threads: 4742 ms (4 seconds) Average: 4 ms per hash MD2: Hashed Password: {MD2}J1+DYe35St5RH5jOIm/ExA== Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms MD5: Hashed Password: {MD5}QvdJref54ZW/R183pEyvyw== Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms PBKDF2WithHmacSHA1/160/128000: Hashed Password: {PBKDF2WithHmacSHA1}AAAAoAAB9ADxgcXJ8tEfLprl6wTPvGvV5tpm+HNgNA 0tmSNC Time for 1000 on 20 threads: 13149 ms (13 seconds) Average: 13 ms per hash PBKDF2WithHmacSHA1/160/720000: Hashed Password: {PBKDF2WithHmacSHA1}AAAAoAAK/IDsOH/sUCqN+1EIVvNCQo/4Kp2OwIUXHs 2L+V/P Time for 1000 on 20 threads: 73623 ms (1 minute 13 seconds) Average: 73 ms per hash PBKDF2WithHmacSHA1/160/1300000: Hashed Password: {PBKDF2WithHmacSHA1}AAAAoAAT1iAdLV/18DqjH0SwjlnbGduXOZKRcYKiWL EYmTGY Time for 1000 on 20 threads: 137559 ms (2 minutes 17 seconds) Average: 137 ms per hash SHA: Hashed Password: {SHA}sumK1vbrhQjdahTPpwS61/Bfb7E= Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms SHA-256: Hashed Password: {SHA-256}AIxwOS46v70PpHu8LtlqqZvUnhWXJ/y6Dy5qvrOp1gE= Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms SHA-384: Hashed Password: {SHA-384}abrlqxaeAO0w0d2YOoy1zt+bVa9HeVMGLDMcEgIN4m4XKRoD3zokw 8UwNLqYhVeu Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms SSHA: Hashed Password: {SSHA}w+MLnmrWdwu/IQ/1kf3ZNLhUKUtusKtwrdp20Q== Time for 1000 on 20 threads: 2 ms Throughput: 500 per ms UFC-CRYPT: Hashed Password: {UFC-CRYPT}umEUxHoYAQmHI Time for 1000 on 20 threads: 29 ms Throughput: 34 per ms
Now when you look at this listing, it becomes easy to see that algorithm choice absolutely impacts your performance characteristics. Many of the insecure hashes handle the 1,000 hashes in a few milliseconds, while the more secure ones have a significant impact.
Let's focus just on the Liferay default, the
PBKDF2WithHmacSHA1
results:
PBKDF2WithHmacSHA1/160/128000: Hashed Password: {PBKDF2WithHmacSHA1}AAAAoAAB9ADxgcXJ8tEfLprl6wTPvGvV5tpm+HNgNA 0tmSNC Time for 1000 on 20 threads: 13149 ms (13 seconds) Average: 13 ms per hash PBKDF2WithHmacSHA1/160/720000: Hashed Password: {PBKDF2WithHmacSHA1}AAAAoAAK/IDsOH/sUCqN+1EIVvNCQo/4Kp2OwIUXHs 2L+V/P Time for 1000 on 20 threads: 73623 ms (1 minute 13 seconds) Average: 73 ms per hash PBKDF2WithHmacSHA1/160/1300000: Hashed Password: {PBKDF2WithHmacSHA1}AAAAoAAT1iAdLV/18DqjH0SwjlnbGduXOZKRcYKiWL EYmTGY Time for 1000 on 20 threads: 137559 ms (2 minutes 17 seconds) Average: 137 ms per hash
Here we can see the impact of adding additional rounds during the hash calculation. 128,000 rounds averages 13 ms per hash, 720,000 is 561% worse clocking in at 73 ms per hash, but 1,300,000 is 1053% worse than 128,000 and 187% worse than 720,000, averaging 137 ms per hash.
So if there are 1,000 users waiting to log in and you only have 20 threads available, they are all logged in in 13 ms at 128k rounds, 1 minute and 13 seconds at 720k, and 2 minutes 17 seconds at 1.3m rounds.
So you might be asking yourself why the heck would you want to increase the rounds to 1,300,000 since every login will take significantly longer to complete?
The Other Shoe
Remember when I shared one of the two things you needed to know before changing your hashing algorithm? The first one was the performance characteristics of the algorithms.
The second thing you need to know is the risk
. All of those really low time, low intensity algorithms
shown above? They are all basically no different than choosing
NONE for your
passwords.encryption.algorithm
(at least from a security perspective).
Think about it, if you're a hacker and you have a list of the ### most frequently used passwords and you can calculate the hash in less than a millisecond and you're comparing against some hash value you have, you must understand that it is only a matter of time before the hacker will figure out a password (or collision) and then be able to get into your system.
Even with a moderately complex password, you could build a program that just starts generating sequences like a, b, c, ... A, B, C, ... 0, 1, 2, ..., aa, ab, ac, ... Aa, Ab, Ac, ... and, with millisecond hash calculation, even the most complex passwords will fall on current hardware, and this is assuming the hacker only has brute force available and not some underground info about how to figure out the hash easier. I mean, just from the example times above, you can see how I could hash 1000 passwords in about 2ms in most cases; this translates into 500,000 hashes each second, 30 million hashes every minute, and a whopping 1,800,000,000 passwords every hour, and 302 trillion passwords in a week... At that rate, I think you can see just how quickly it would be possible to derive all of the hashes necessary to get access to at least one account in your environment, if not more... And these numbers were based off of my system running with only 20 threads; an AWS compute system would likely blow these numbers away (although you'll pay for that privilege)...
So ultimately the risk comes down to this:
1. If the hash value is somehow leaked, i.e. a developer puts it
in a comment in the code, or someone forgets to protect your cloud
database, or a hacker gets in and dumps your User_
table,
how hard is it going to be for the hacker to figure out the original password?
2. If the hacker can figure out the password, what is the potential risk? I ask this because, well if I'm just hosting a site of recipes, a discovered password is certainly going to give me a lot of headaches, but that is quite different than if I'm running an online crypto wallet site where users are holding their crypto and a discovered password could results in losses of significant money.
Most organizations are going to fall somewhere between hosting recipes and online crypto wallets. Where your organization lands on this scale will determine what risk you have and, if your risk is on the high end, regardless of the performance characteristics, you're going to want to consider a more complex hash algorithm.
Complexity != Slow
Actually Olaf and I got into an argument about this...
He was arguing that as you add rounds (complexity) to a hashing algorithm, there will of course be an additional penalty that you have no choice but to pay. There's no exception, you have to pay the bill as it comes due.
I however was looking at it differently; adding complexity does not necessarily translate into being slow. I knew that there were alternative implementations out there, including Bouncy Castle. And that's really what I wanted to know - was there a better implementation out there that offered the same protection but was better optimized for the hash calculation.
I was curious as to whether it was possible to provide an alternative implementation that could still have the same level of complexity (rounds) yet not be as slow.
So yeah, I built one (also included in the Github repo).
Called the BCPBKDF2WithHmacSHA1
(unique, yeah? I just
prefixed with BC to indicate it was BouncyCastle...), it is an
implementation of the PasswordEncryptor
interface, but it
leveraged Bouncy Castle's implementation of the
PBKDF2WithHmacSHA1
algorithm.
So how did it perform? Check it for yourself:
g! hash:simplebcpbkdf2 Password123 1000 20 BCPBKDF2WithHmacSHA1/160/128000: Hashed Password: {BCPBKDF2WithHmacSHA1}AAAAoAAB9AAgLw0eOfAPvMhA8ZEpy8qEQSr++/kaB GGJmwa8 Time for 1000 on 20 threads: 8245 ms (8 seconds) Average: 8 ms per hash BCPBKDF2WithHmacSHA1/160/720000: Hashed Password: {BCPBKDF2WithHmacSHA1}AAAAoAAK/IDmPGuMG/dbK9c72gb1KRChlw+MBc5RJl RNFe0d Time for 1000 on 20 threads: 46757 ms (46 seconds) Average: 46 ms per hash BCPBKDF2WithHmacSHA1/160/1300000: Hashed Password: {BCPBKDF2WithHmacSHA1}AAAAoAAT1iDyKFUKOnsrWt/nxusNnOmd0hSczw6l B6wEgojP Time for 1000 on 20 threads: 85824 ms (1 minute 25 seconds) Average: 85 ms per hash
Using Bouncy Castle's implementation, I saw about a 38% reduction on all three rounds: 128,000, 720,000, and 1,300,000 rounds.
Take note of the new hash command, hash:simplebcpbkdf2
password hashes threads
, there's also a
hash:bcpbkdf2 password keySize rounds
iterations
for individual testing.
Using This Was Another Story...
Boy, you can't believe how excited I was to try out this new implementation...
Since it was based upon Liferay's
PBKDF2PasswordEncryptor
, I already supported the
arguments for key size and rounds, and I registered my implementation
using the type BCPBKDF2
(Liferay's uses the type
PBKDF2
, so again I was copying them).
With my component built and deployed, it was time to update my hash gogo command.
The key part of leveraging all of these different Liferay
algorithms was simply to call
PasswordEncryptorUtil.encrypt(algorithm, password, (String)
null);
I could do this, for example like
PasswordEncryptorUtil.encrypt("PBKDF2WithHmacSHA1/160/720000",
"Password123", (String) null);
and it would
magically work.
So I basically changed so I was
using PasswordEncryptorUtil.encrypt("BCPBKDF2WithHmacSHA1/160/720000",
"Password123", (String) null);
and darn it, I just
got NPEs. Why? Because Liferay couldn't find my password encryptor.
Tracing through the stack trace, I found that the
com.liferay.portal.security.password.encryptor.internal.CompositePasswordEncryptor
class had a _select(String algorithm)
method to select
the password encryptor based on the algorithm passed in. And wouldn't
you know it, Liferay has special code in that class that says if the
algorithm STARTS WITH "PBKDF2
",
regardless of what follows, it was going to use the password encryptor
with the type "PBKDF2
". The other algorithms,
well those had to be an exact match on the type the password
encryptor component was assigned.
Cool, I just changed my line then to be
PasswordEncryptorUtil.encrypt("BCPBKDF2/160/720000",
"Password123");
since my type is
"BCPBKDF2
" and expected it to work, but no, it
failed. Remember the "exact match" thing above? Well, that
meant that I could not use parameterized password encryptors, so I
couldn't allow for different key sizes or rounds...
So I opened a feature request ticket, https://issues.liferay.com/browse/LPS-175161,
so that CompositePasswordEncryptor
would ignore the
arguments when looking for the PasswordEncryptor
to use.
I have the code changed and a PR ready to submit to Liferay, but in
the mean time I still wanted to get it working for the repo, so just
as I wrote in https://liferay.dev/blogs/-/blogs/extending-liferay-osgi-modules-revisited,
I created an override module so I could use a custom
CompositePasswordEncryptor
that ignored the parameters.
This module is not included in the Github repo
because I ended up creating the Alternative Usage in the next section
which was a better implementation.
And so, once this was all in place, my hash gogo command started working and I could gather the performance numbers that I shared above.
I didn't know when I was going through all of this if it was going to be worth it, whether Bouncy Castle would prove the better implementation or not. Thankfully though I found almost a 40% improvement when using Bouncy Castle, so it ended up being well worth the effort.
Alternative Usage
So, when I got through all of this, I had it all working, but boy, was it a kludge using the marketplace override approach.
I aspect I suspected, but was not 100% sure of, that the JCE
implementation of PBKDF2WithHmacSHA1
was computing the
exact same value as the Bouncy Castle implementation, and I needed to
prove it.
So I added a new command, hash:verifyPbkdf2
password
that would compute the hash using both
algorithms and verify they are equal. To do this, I basically have a
bunch of lines like:
String pbkdf2Hash = PasswordEncryptorUtil.encrypt( PasswordEncryptor.TYPE_PBKDF2 + "WithHmacSHA1/160/128000", password, (String) null); String bcHash = PasswordEncryptorUtil.encrypt("BCPBKDF2WithHmacSHA1/160/128000", password, pbkdf2Hash); boolean same = pbkdf2Hash.equals(bcHash);
Since I pass the encrypted hash from the first call as the last argument on the second call, I'm providing the Bouncy Castle implementation with the same key size, rounds and salt, so it basically has no choice but to generate the same hash result.
The new hash:verifyPbkdf2
command will check for
the three common rounds, 128,000, 720,000 and 1,300,000 rounds.
After proving Bouncy Castle generates the same hashes yet with
better performance, that meant that I could actually register a BC
implementation using Liferay's PBKDF2 type and provide a higher
service ranking. I called this component class
BouncyCastleReplacementPBKDF2PasswordEncryptor
. I still
suspect though that even with the higher service ranking, potentially
Liferay's implementation could still be available. If you do want to
use this component to replace Liferay's implementation, I strongly
suggest that you blocklist the
com.liferay.portal.security.password.encryptor.internal.PBKDF2PasswordEncryptor
component. That should prevent Liferay's from starting, leaving only
the Bouncy Castle implementation for the system to use.
Choosing the Right Algorithm
When choosing a hashing algorithm for your implementation, it is important to consider both performance and security. On the one hand, you want the algorithm to be fast enough to handle large volumes of logins and meet the performance requirements of your site. On the other hand, you also want the algorithm to be secure enough to protect user passwords from unauthorized access and tampering.
One way to evaluate the performance of a hashing algorithm is to consider its hash length, computational complexity, and the speed of the hash computation. Algorithms with a shorter hash length, such as MD5, are generally faster but also less secure, as they have a higher risk of hash collisions and are more susceptible to attacks such as birthday attacks. In contrast, algorithms with a longer hash length, such as SHA-256, SHA-384 and PBKDF2, offer better security but may be slower in terms of performance.
Another factor to consider when choosing a hashing algorithm is its resistance to attacks. For example, algorithms that are considered "broken" or "deprecated" should generally be avoided, as they may be vulnerable to attacks such as collisions, preimage attacks, or differential attacks. On the other hand, newer and more secure algorithms, such as SHA-384 and PBKDF2, have been designed to be more resistant to these types of attacks and may offer better security for your implementation.
Ultimately, the right balance between performance and security will depend on the specific requirements of your implementation and the importance of the passwords. To evaluate this criteria, you should consider factors such as the volume of expected logins (passwords) to be hashed, the risk associated with the exposure of account password hashes, the security requirements of your site, and any legal or regulatory compliance requirements. Based on these factors, you can choose an algorithm that offers the right balance of performance vs security for your implementation.
How to Change Algorithms
Okay, so let's say you have decided that you want to change up
the algorithm. Liferay's latest bundle was switched to
PBKDF2WithHmacSHA1/160/1300000
and you've decided that
your site is going to be find at
PBKDF2WithHmacSHA1/160/128000
. But since users have
already been logging in, their passwords have been hashed already and,
since it is 1-way only, you can't redo the hashes yourself, what are
your options?
Well, unfortunately I have bad news for you - changing the algorithm will have no impact on current users, even if they change their password on their own. Their password will tend to stick with the same algorithm, key size and rounds from their previous password rather than just switching over.
Now you can force the issue, if you want. You can set the password reset flags on the users and clear the encrypted passwords (you must clear the passwords out, otherwise Liferay will still use the keySize and rounds from the current password even though you have changed the default), this way they'll be forced to enter new passwords, but likely this is going to be problematic for you as it is not a great UX and sometimes not even an option depending upon your business. But it is truly the only way you're going to know that you're not leaving a password around with the old hash algorithm.
One option I'd encourage you to do is to go and add your vote to As a System Administrator, I want to trigger a password hashing migration process (LPS-115867). This ticket has unfortunately been delayed many times but a show of support (via votes) may be enough to get it moving again.
And of course, there's always a customization out there that can get the job done. I haven't tried one yet, but if I do, I'll add it to the Github repo.
Conclusion
Wow, what a wild ride, wasn't it?
Hopefully you've learned a lot about how Liferay is handling passwords, how the choice of hashing algorithm can definitely impact your performance, how you can change the default, but mostly the things you need to know in order to pick the right algorithm for your implementation.
You're probably going to want to check out the Github repo, especially if you're interested in using the faster Bouncy Castle PBKDF2 hashing and minimize your impact on the hashing rounds changes.
Be aware though that ultimately the prediction for quantum computers have them arriving in anywhere between 10-20 years and, when that happens, it won't matter how complex your current algorithm is or how many rounds you use. The prediction is that quantum computers will be able to brute force these calculations such that none of our current or near future hashing algorithms will be able to stand against them. Not that means we shouldn't be trying to secure our data, I'm just suggesting that in the long run things will keep changing and the war with hackers will continue well into the future.
Anyhow, let me know how the code in the repo works out for you!
Update
So I've been sharing my results with the Liferay security team. They've asked me to submit a PR for switching to Bouncy Castle on ticket LPS-175308... So hopefully, some time in the future, we'll get the Bouncy Castle version into core. Even when that happens, it won't invalidate anything that is in the repo, that code will still work alongside Liferay's class.