In my last post, I discussed four cryptographic hashing functions that are suitable for password storage. I pointed out that Argon2 (in particular Argon2id) is the hashing function that leading cryptographers around the world recommend. Simply choosing to use Argon2 for password storage is only half of the battle.
Argon2 accepts three parameters you can use to tune the algorithm to your needs. In this post, I’ll discuss these three parameters and hopefully give you the tools and knowledge to show you how to choose the best parameters for your scenario.
What’s the Goal?
As I’ve discussed in ad nauseam, storing passwords in a system poses countless security threats. Storing strong, cryptographic hashes of passwords in a database is one mitigation against an attacker who is able to breach your system. But it doesn’t stop here.
There is a delicate balance between execution time and required resources when calculating a password hash. Your goal is to maximize the time it takes to calculate a hash to prevent an attacker from performing a brute force attack. However, your goal is to also minimize the cost to your system so that your product doesn’t come to a grinding halt when someone wants to login.
Argon2 allows you to manipulate three parameters in an effort to find this sweet spot:
- The number of iterations, or passes, that the algorithm will make over memory, an increase in such yields additional protection against tradeoff attacks.
- The amount of memory to utilize, with more memory protecting against customized hardware attacks.
- The degree of parallelism, or the number of concurrent threads, that the algorithm will utilize to compute the hash.
Choosing Correct Parameters
Choose the Execution Time
Before you can begin to choose the parameters for Argon2, you need to consider what the execution time of the algorithm needs to be. This will unfortunately create some tension with recommended UX response times. Unfortunately (and fortunately) for the user, security really needs to win out here.
The Argon2 documentation recommends 0.5 seconds for authentication. Libsodium’s documentation recommends 1 second for web applications and 5 seconds for desktop applications. I personally wouldn’t recommend anything more than 1 second or your users will hate you and logins will end up DoS’ing your application. Anything less than 0.5 seconds is a certain security failure.
Choose the Degree of Parallelism
According to the Argon2 documentation, the degree of parallelism you choose should be dependent upon the number of CPU cores on your authentication server. Simply double the number of cores to determine the degree of parallelism. For example, if your authentication server CPU has four cores, then you should choose a degree of parallelism of eight.
Choose the Amount of Memory
The cost of computing a password hash should be “dominated by memory-related costs“. The more memory you throw at Argon2, the more resources the algorithm takes up and the more computationally expensive the process. The only way that an attacker can negate this is to purchase more memory. The hardware requirements quickly make the cost prohibitive.
Because of this, your goal should be to start calibrating Argon2 with the largest memory usage possible. Then, work your way down until you achieve the execution time required. Once you find the memory usage required to achieve the execution time, you are at a good starting point for the next step.
Choose the Number of Iterations
Now that you have identified parallelism and memory usage, simply increase the number of iterations until the algorithm exceeds execution time. For example, if your maximum execution time is 2 seconds, 2 iterations cost you 1.9 seconds, and 3 iterations cost you 2.1 seconds, choose 2 iterations.
From what I can find, there isn’t a documented minimum number of iterations. The Argon2 documentation does allude that a single iteration with higher memory usage is acceptable. Some contributors to the algorithm recommend a minimum of 2 iterations. Argon2’s benchmark utility utilizes 3 iterations. Libsodium’s implementation utilizes 4 iterations for sensitive operations. And the OWASP password storage cheat sheet is out there in left field by recommending upwards of 40 iterations.
Unfortunately, there is no clear guidance and I am no cryptographer. Libsodium, however, is held in quite high respect, so a minimum number of 4 iterations is probably conservative. Thus, if Argon2 completes the password hashing algorithm in an acceptable amount of time using less iterations, decrease the memory usage and then increase the number of iterations until you reach the execution time limit. Repeat this process until you surpass 4 iterations.
An Easier Way
I’ve taken the liberty to throw together a small .NET Core command line application that eases the burden of choosing the correct parameters. You can download the code from GitHub here.
Simply execute the following commands to clone the repo and perform the Argon2 calibration function:
git clone https://github.com/bburman/Twelve21.PasswordStorage.git cd ./Twelve21.PasswordStorage dotnet build cd ./Twelve21.PasswordStorage dotnet run a2c
By default, the application will use a degree of parallelism based on the number of CPU cores. It will use a minimum of two iterations and a maximum of 1 second for execution time. Use the -h parameter to see a list of options that you can use to fine tune the algorithm. The application will run and show you the best results, similar to:
Best results: M = 256 MB, T = 2, d = 8, Time = 0.732 s M = 128 MB, T = 6, d = 8, Time = 0.99 s M = 64 MB, T = 12, d = 8, Time = 0.968 s M = 32 MB, T = 24, d = 8, Time = 0.896 s M = 16 MB, T = 49, d = 8, Time = 0.973 s M = 8 MB, T = 96, d = 8, Time = 0.991 s M = 4 MB, T = 190, d = 8, Time = 0.977 s M = 2 MB, T = 271, d = 8, Time = 0.973 s M = 1 MB, T = 639, d = 8, Time = 0.991 s
It’s important to note that this application needs to executed on the server under the configuration for which you intend to perform authentication. The above was executed on a business-grade company laptop, not an application server. Results will vary and you need to select parameters that will match your production environment.
An Alternative Method
Alternatively, you can utilize a library, such as Libsodium. A popular library, such as this, has reasonable parameters built into the platform. This not only reduces the choices that you have to make as a developer, but also prohibits someone from making a blatant mistake. Using a library comes with the added bonus that, if a library is ever updated, simply pulling in the update will yield more accurate parameters.
The downside is that, if you know what you are doing, it’s difficult to get the fine-grained control over the algorithm that you might need. Still, choosing reasonable defaults from a library such as Libsodium is a much better alternative to using blatantly bad parameter choices.
This blog post discussed the parameters that the Argon2 algorithm takes as input. It also discussed the trade-off between these parameters with regards to execution time. Generally a higher execution time will yield a tougher password hash to brute-force, but it could also cripple your system. Hopefully this post helps you determine parameters that are optimized for your environment while also making it more difficult for an attacker.
In a future post, we’ll discuss how to properly use Argon2 from both .NET and Java applications. So stick around. And, as always, if you have questions or comments, please leave me a comment below.
The “git clone https://https://github.com/bburman/Twelve21.PasswordStorage.git” has a too many “https”…
I apologize. I had a typo in the above command line. It should be fixed now.
Thanks for bringing this to my attention!
Hmm, for me it is not clear how the result of your tool maps to the 3 parameters “Parallelism”, “Memory” and “Iterations”. May I ask you to add a legend to the end of your tool, in the style of:
M=Memory (1MB = 1048576 bytes)
d=(Number of threads?)
I agree. A legend would be helpful.
M = Memory Usage, in MB
T = Number of Iterations
D = Degree of Parallelism
Please let me know if you have any additional questions. Thanks for the feedback!
Thanks Bryan! Very helpful.
A library that implements Argon2id like: https://github.com/mheyman/Isopoh.Cryptography.Argon2
has lanes as a factor and that is messing up the whole calculation (if it wasn’t the library itself clocking at a different speed even though that would be strange)
I might take the code and fork it into using this other library and add lanes into consideration to see.