Prove who the user is โ without ever storing their password.
1a
Every protected request passes two separate checks, always in this order.
1b
bcrypt turns a password into a salted, deliberately expensive hash. You store only the hash.
import "golang.org/x/crypto/bcrypt" const cost = 12 // ~270 ms on this machine func HashPassword(pw string) (string, error) { h, err := bcrypt.GenerateFromPassword( []byte(pw), cost) return string(h), err } func CheckPassword(hash, pw string) bool { err := bcrypt.CompareHashAndPassword( []byte(hash), []byte(pw)) return err == nil }
hash 1: $2a$12$EcX1op6oy.w0pO8mONBPr.6ZSuiU7NbxZOJ77Mqqvt/Ef5l12frhy
hash 2: $2a$12$q4/EfxVA3zOi64hBhpAxXeURCRWl3l3b9ZdDIQpBV01ZUv4mcOd7K
same? false
right password: <nil>
wrong password: crypto/bcrypt: hashedPassword is not the hash of the given password
73-byte password: bcrypt: password length exceeds 72 bytes
Hashing passwords with SHA-256. It is built to be fast โ a GPU tries billions per second. For passwords, slow is the feature.
1c
A normal == stops at the first wrong byte. How long it takes tells an attacker how much they got right.
import "crypto/subtle" func validKey(got, want string) bool { return subtle.ConstantTimeCompare( []byte(got), []byte(want)) == 1 }
wrong at byte 0 naive 1ns constant 101ns wrong at byte 128 naive 52ns constant 101ns wrong at byte 255 naive 85ns constant 101ns # naive time grows with the correct prefix; constant never moves
subtle.ConstantTimeCompare for anything secret you compare yourself: API keys, reset tokens, signatures.bcrypt.CompareHashAndPassword is already constant-time.invalid email or password.POST /register โ hash with cost 12, store only the hash.POST /login โ compare; one error message for every failure.Recap