Not sure how to structure your Go web application?

My new book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

How to Hash and Verify Passwords With Argon2 in Go

Published on:

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.

Choosing Parameters

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:

  1. 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.
  2. Increase the number of iterations until you reach your maximum runtime limit (for example, 500ms).
  3. If you're already exceeding the your maximum runtime limit with the number of iterations = 1, then you should reduce the memory parameter.

Hashing Passwords

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:

$ go get golang.org/x/crypto/argon2

And you can use it to hash a specific password like so:

File: main.go
package main

import (
    "crypto/rand"
    "fmt"
    "log"

    "golang.org/x/crypto/argon2"
)

type params struct {
    memory      uint32
    iterations  uint32
    parallelism uint8
    saltLength  uint32
    keyLength   uint32
}

func main() {
    // Establish the parameters to use for Argon2.
    p := &params{
        memory:      64 * 1024,
        iterations:  3,
        parallelism: 2,
        saltLength:  16,
        keyLength:   32,
    }

    // Pass the plaintext password and parameters to our generateFromPassword
    // helper function.
    hash, err := generateFromPassword("password123", p)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(hash)
}

func generateFromPassword(password string, p *params) (hash []byte, err error) {
    // Generate a cryptographically secure random salt.
    salt, err := generateRandomBytes(p.saltLength)
    if err != nil {
        return nil, err
    }

    // Pass the plaintext password, salt and parameters to the argon2.IDKey
    // function. This will generate a hash of the password using the Argon2id
    // variant.
    hash = argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)

    return hash, nil
}

func generateRandomBytes(n uint32) ([]byte, error) {
    b := make([]byte, n)
    _, err := rand.Read(b)
    if err != nil {
        return nil, err
    }

    return b, nil
}

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:

$ go run main.go
[9 18 35 54 101 221 120 189 57 241 229 248 140 1 102 58 93 211 115 49 131 162 24 50 167 142 227 198 85 186 200 248]

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.

Storing Passwords

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:

$argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG

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.
  • $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:

File: main.go
package main

import (
    "crypto/rand"
    "encoding/base64"
    "fmt"
    "log"

    "golang.org/x/crypto/argon2"
)

...

func generateFromPassword(password string, p *params) (encodedHash string, err error) {
    salt, err := generateRandomBytes(p.saltLength)
    if err != nil {
        return "", err
    }

    hash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)

    // Base64 encode the salt and hashed password.
    b64Salt := base64.RawStdEncoding.EncodeToString(salt)
    b64Hash := base64.RawStdEncoding.EncodeToString(hash)

    // Return a string using the standard encoded hash representation.
    encodedHash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p.parallelism, b64Salt, b64Hash)

    return encodedHash, nil
}

...

And if you run the code again now, the output should look similar to this:

$ go run run.go
$argon2id$v=19$m=65536,t=3,p=2$Woo1mErn1s7AHf96ewQ8Uw$D4TzIwGO4XD2buk96qAP+Ed2baMo/KbTRMqXX00wtsU

Verifying Passwords

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:

  1. Extract the salt and parameters from the encoded password hash stored in the database.
  2. Derive the hash of the plaintext password using the exact same Argon2 variant, version, salt and parameters.
  3. Check whether this new hash is the same as the original one.

You can implement this like so:

File: main.go
package main

import (
    "crypto/rand"
    "crypto/subtle"
    "encoding/base64"
    "errors"
    "fmt"
    "log"
    "strings"

    "golang.org/x/crypto/argon2"
)

var (
    ErrInvalidHash         = errors.New("the encoded hash is not in the correct format")
    ErrIncompatibleVersion = errors.New("incompatible version of argon2")
)

type params struct {
    memory      uint32
    iterations  uint32
    parallelism uint8
    saltLength  uint32
    keyLength   uint32
}

func main() {
    p := &params{
        memory:      64 * 1024,
        iterations:  3,
        parallelism: 2,
        saltLength:  16,
        keyLength:   32,
    }

    encodedHash, err := generateFromPassword("password123", p)
    if err != nil {
        log.Fatal(err)
    }

    match, err := comparePasswordAndHash("password123", encodedHash)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Match: %v\n", match)
}

...

func comparePasswordAndHash(password, encodedHash string) (match bool, err error) {
    // Extract the parameters, salt and derived key from the encoded password
    // hash.
    p, salt, hash, err := decodeHash(encodedHash)
    if err != nil {
        return false, err
    }

    // Derive the key from the other password using the same parameters.
    otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)

    // Check that the contents of the hashed passwords are identical. Note
    // that we are using the subtle.ConstantTimeCompare() function for this
    // to help prevent timing attacks.
    if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
        return true, nil
    }
    return false, nil
}

func decodeHash(encodedHash string) (p *params, salt, hash []byte, err error) {
    vals := strings.Split(encodedHash, "$")
    if len(vals) != 6 {
        return nil, nil, nil, ErrInvalidHash
    }

    var version int
    _, err = fmt.Sscanf(vals[2], "v=%d", &version)
    if err != nil {
        return nil, nil, nil, err
    }
    if version != argon2.Version {
        return nil, nil, nil, ErrIncompatibleVersion
    }

    p = &params{}
    _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)
    if err != nil {
        return nil, nil, nil, err
    }

    salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
    if err != nil {
        return nil, nil, nil, err
    }
    p.saltLength = uint32(len(salt))

    hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
    if err != nil {
        return nil, nil, nil, err
    }
    p.keyLength = uint32(len(hash))

    return p, salt, hash, nil
}

If you run this code now, you should get a positive match when comparing the plaintext and hashed password and see output like this:

$ go run main.go
Match: true

If you change the plaintext password used in one of the function calls, like so:

File: main.go
package main

...

func main() {
    p := &params{
        memory:      64 * 1024,
        iterations:  3,
        parallelism: 2,
        saltLength:  16,
        keyLength:   32,
    }

    encodedHash, err := generateFromPassword("password123", p)
    if err != nil {
        log.Fatal(err)
    }

    // Use a different password...
    match, err := comparePasswordAndHash("pa$$word", encodedHash)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Match: %v\n", match)
}

...

Then running the code should result in a negative match:

$ go run main.go
Match: false

The complete sample code for this post is available in this gist.

Update: I've created a package at github.com/alexedwards/argon2id, which you can use instead of implementing the patterns in this post yourself.