Storing Passwords

Store a password as a bcrypt hash with a cost you can raise later, and never design a hashing scheme yourself.

Syntaxhash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

A password must never be stored the way the user typed it. If the database leaks, every account is open at once, and most people reuse passwords elsewhere. Instead you store a hash. A hash is a fixed-size scramble of the password that cannot be turned back into it. At login you scramble what was typed and compare the two scrambles.

A fast hash is not enough. Something like SHA-256 can be computed billions of times a second, so a leaked list of hashes can be guessed through word by word. bcrypt is built to be slow on purpose. It takes a cost, and every step up in cost doubles the work. It also mixes in a salt: random bytes stored inside the hash itself, so two people who chose the same password still get different hashes.

How it is used

bcrypt lives in golang.org/x/crypto/bcrypt, added with go get golang.org/x/crypto. Hashing is bcrypt.GenerateFromPassword(password []byte, cost int) ([]byte, error). Checking is bcrypt.CompareHashAndPassword(hashedPassword, password []byte) error, which returns nil when they match. The hash is 60 bytes of printable text beginning $2a$, so the database column needs room for at least 60 characters.

The same cost, written two ways

bcrypt.DefaultCost is the number 10. Passing bcrypt.DefaultCost and passing 10 are the same call. A cost below bcrypt.MinCost, which is 4, is silently replaced by DefaultCost rather than rejected.

What trips people up

Do not compare two hashes with ==, because the salt differs every time. Do not hash in the browser. Do not assemble your own scheme out of parts you recognise, such as SHA-256 with a salt you invented. The slowness is the point, and a hand-rolled version does not have it.

Example

Example · go
package main

import (
	"errors"
	"fmt"

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

func main() {
	// The hash is different on every run because bcrypt mixes in random salt,
	// so it is never printed and never compared with ==.
	hash, err := bcrypt.GenerateFromPassword([]byte("correct horse battery"), bcrypt.DefaultCost)
	if err != nil {
		fmt.Println("hashing failed:", err)
		return
	}

	// Cost reads the cost back out of a stored hash.
	cost, err := bcrypt.Cost(hash)
	if err != nil {
		fmt.Println("unreadable hash:", err)
		return
	}
	fmt.Println("cost:", cost)

	// A nil error means the password matches the hash.
	err = bcrypt.CompareHashAndPassword(hash, []byte("correct horse battery"))
	fmt.Println("right password accepted:", err == nil)

	err = bcrypt.CompareHashAndPassword(hash, []byte("guess"))
	fmt.Println("wrong password rejected:", errors.Is(err, bcrypt.ErrMismatchedHashAndPassword))
}

// Output:
// cost: 10
// right password accepted: true
// wrong password rejected: true

When to use it

  • Hash a new password in a registration handler before it reaches the database.
  • Verify a submitted password against the stored hash in a login handler.
  • Raise the bcrypt cost and upgrade each user's hash the next time they sign in.
  • Replace an inherited MD5 password column without asking every user to reset.

More examples

Register and log in, with the same work either way

The two halves of password handling in one file: hash on register, compare on login. The throwaway hash is there because bcrypt is slow. If you skipped the comparison for an unknown email, that reply would come back noticeably faster, and anyone could use the login form to find out which email addresses are registered. Reach for this shape whenever you write a sign-in endpoint.

Example · go
package main

import (
	"fmt"

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

// users maps an email to the stored hash. A real app uses a database table.
var users = map[string][]byte{}

func register(email, password string) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	users[email] = hash
	return nil
}

// login always runs exactly one bcrypt comparison. When the email is unknown it
// compares against a throwaway hash, so an unknown email takes the same time as
// a known one and the reply does not reveal who has an account.
func login(email, password string, unknown []byte) bool {
	hash, ok := users[email]
	if !ok {
		hash = unknown
	}
	return bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil
}

func main() {
	unknown, err := bcrypt.GenerateFromPassword([]byte("nobody uses this"), bcrypt.DefaultCost)
	if err != nil {
		fmt.Println(err)
		return
	}
	if err := register("[email protected]", "s3cret-passphrase"); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(login("[email protected]", "s3cret-passphrase", unknown))
	fmt.Println(login("[email protected]", "wrong", unknown))
	fmt.Println(login("[email protected]", "anything", unknown))
}

// Output:
// true
// false
// false

Raise the cost and upgrade hashes as people log in

Hashes made years ago are weaker than hashes made today, and you cannot re-hash without the password. This is the fix: on a successful login, if the stored cost is below current policy, hash again at the new cost and save the result. You would reach for the same shape to migrate off an inherited MD5 or SHA-1 column, by checking the old format first and writing a bcrypt hash in its place.

Example · go
package main

import (
	"fmt"

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

// wantCost is today's policy. It goes up as servers get faster.
const wantCost = 12

// check verifies the password, and returns a replacement hash when the stored
// one was made at a lower cost. Login is the only moment the plain password is
// in hand, so it is the only chance to re-hash it.
func check(stored []byte, password string) (newHash []byte, ok bool) {
	if err := bcrypt.CompareHashAndPassword(stored, []byte(password)); err != nil {
		return nil, false
	}
	cost, err := bcrypt.Cost(stored)
	if err != nil || cost >= wantCost {
		return nil, true
	}
	upgraded, err := bcrypt.GenerateFromPassword([]byte(password), wantCost)
	if err != nil {
		return nil, true // the old hash still works; try again next login
	}
	return upgraded, true
}

func main() {
	old, err := bcrypt.GenerateFromPassword([]byte("passphrase"), bcrypt.DefaultCost)
	if err != nil {
		fmt.Println(err)
		return
	}

	upgraded, ok := check(old, "passphrase")
	fmt.Println("accepted:", ok)
	if upgraded != nil {
		cost, err := bcrypt.Cost(upgraded)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Println("rehashed at cost:", cost)
	}
}

// Output:
// accepted: true
// rehashed at cost: 12

The 72-byte limit is an error, not a truncation

bcrypt only takes 72 bytes of input. golang.org/x/crypto/bcrypt refuses a longer password with ErrPasswordTooLong rather than quietly cutting it short. Password managers generate passphrases longer than that, so without this check a small number of users simply cannot register and you get a bug report about a broken sign-up page. Note the units: 72 bytes, not 72 characters, so accented letters and emoji count for more than one.

Example · go
package main

import (
	"errors"
	"fmt"
	"strings"

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

func main() {
	// 80 characters, which is longer than bcrypt's 72-byte input limit.
	long := strings.Repeat("a", 80)

	_, err := bcrypt.GenerateFromPassword([]byte(long), bcrypt.DefaultCost)
	fmt.Println("rejected:", errors.Is(err, bcrypt.ErrPasswordTooLong))

	// So check the length while validating the form and say so in plain words,
	// instead of letting the registration fail as a server error.
	fmt.Println("over limit:", len(long) > 72)
}

// Output:
// rejected: true
// over limit: true

Discussion

  • Be the first to comment on this lesson.