How to Hash and Verify Passwords With Argon2 in Go
Thanks to Andreas Auernhammer, author of the golang.org/x/crypto/argon2 package, for checking over this post before publication.
If you're planning to store user passwords it's good practice (essential really) to hash them using a computationally expensive key-derivation function (KDF) like Bcrypt, Scrypt or Argon2.
Hashing and verifying passwords in Go with Bcrypt and Scrypt is already easy to do thanks to the golang.org/x/crypto/bcrypt package and Matt Silverlock's elithrar/simple-scrypt package. I recommend them both.
If you want to use Argon2 — which is widely considered to be the best in class KDF for hashing passwords — then you've got a couple of choices.
The tvdburgt/go-argon2 package provides Go bindings to the
libargon2 C library, or you can implement a pure Go solution by wrapping the golang.org/x/crypto/argon2 package with helpers for hashing and verifying passwords. In the rest of this post I'm going to explain exactly how to use this pure Go approach.
A Brief Introduction to Argon2
But first, a little bit of background.
It's important to explain that the Argon2 algorithm has 3 variants which work slightly differently: Argon2d, Argon2i and Argon2id. In general, for password hashing you should use the Argon2id variant. This is essentially a hybrid of the Argon2d and Argon2i algorithms and uses a combination of data-independent memory access (for resistance against side-channel timing attacks) and data-depending memory access (for resistance against GPU cracking attacks).
The Argon2 algorithm accepts a number of configurable parameters:
- Memory — The amount of memory used by the algorithm (in kibibytes).
- Iterations — The number of iterations (or passes) over the memory.
- Parallelism — The number of threads (or lanes) used by the algorithm.
- Salt length — Length of the random salt. 16 bytes is recommended for password hashing.
- Key length — Length of the generated key (or password hash). 16 bytes or more is recommended.
The memory and iterations parameters control the computational cost of hashing the password. The higher these figures are, the greater the cost of generating the hash. It also follows that the greater the cost will be for any attacker trying to guess the password.
But there's a balance that you need to strike. As you increase the cost, the time taken to generate the hash also increases. If you're generating the hash in response to a user action (like signing up or logging in to a website) then you probably want to keep the runtime to less than 500ms to avoid a negative user experience.
If the Argon2 algorithm is running on a machine with multiple cores, then one way to decrease the runtime without reducing the cost is to increase the parallelism parameter. This controls the number of threads that the work is spread across. There's an important thing to note here though: changing the value of the parallelism parameter changes the output of the algorithm. So — for example — running Argon2 with a parallelism parameter of 2 will result in a different password hash to running it with a parallelism parameter of 4.
Picking the right parameters for Argon2 depends heavily on the machine that the algorithm is running on, and you'll probably need to do some experimentation in order to set them appropriately.
The recommended process for choosing the parameters can be paraphrased as follows:
- Set the parallelism and memory parameters to the largest amount you are willing to afford, bearing in mind that you probably don't want to max these out completely unless your machine is dedicated to password hashing.
- Increase the number of iterations until you reach your maximum runtime limit (for example, 500ms).
- If you're already exceeding the your maximum runtime limit with the number of iterations = 1, then you should reduce the memory parameter.
Now that those explanations are out of the way let's jump into writing the code to hash a password with Argon2.
First, you'll need to
go get the golang.org/x/crypto/argon2 package which implements the Argon2 algorithm:
And you can use it to hash a specific password like so:
A quick note on terminology and naming. Formally, Argon2 is a key-derivation function and it produces a key derived from the provided password and salt. This derived key is our 'hashed password'.
The other important thing to point out here is the
generateRandomBytes() function. In this we're using Go's crypto/rand package to generate a cryptographically secure random salt, rather than using a fixed salt or a pseudo-random salt.
If you run the program at this point it should print a slice containing the bytes of the hashed password, similar to this:
Each time you run the program you'll see that it results in a completely different output for the same password, thanks to the addition of our random salt.
So, creating a hashed password with some specific parameters is straightforward enough. But in most cases you'll want to store the salt and specific parameters that you used alongside the hashed password, so that it can be reproducibly verified at a later point.
The standard way to do this is to create an encoded representation of the hashed password which looks like this:
Let's break down what this represents:
$argon2id— the variant of Argon2 being used.
$v=19— the version of Argon2 being used.
$m=65536,t=3,p=2— the memory (
m), iterations (
t) and parallelism (
p) parameters being used.
$c29tZXNhbHQ— the base64-encoded salt, using standard base64-encoding and no padding.
$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG— the base64-encoded hashed password (derived key), using standard base64-encoding and no padding.
Let's update the
generateHash() function so that it returns a string in this format:
And if you run the code again now, the output should look similar to this:
The final aspect to cover is how to verify passwords.
In most cases, you'll take the encoded password hash that we've just produced and store it in a database of some kind. Then at a later point, you'll want to check whether a plaintext password provided by a user matches the one represented by the encoded password hash.
In essence, the steps to do this check are:
- Extract the salt and parameters from the encoded password hash stored in the database.
- Derive the hash of the plaintext password using the exact same Argon2 variant, version, salt and parameters.
- Check whether this new hash is the same as the original one.
You can implement this like so:
If you run this code now, you should get a positive match when comparing the plaintext and hashed password and see output like this:
If you change the plaintext password used in one of the function calls, like so:
Then running the code should result in a negative match:
The complete sample code for this post is available in this gist.