Skip to content
Snippets Groups Projects
Commit 3a206d00 authored by ale's avatar ale
Browse files

Nicer API for pwhash, and a benchmarking tool

The API makes it easier to set custom parameters on hashers. Default
Argon2 parameters were also set to defaults more suited for a
high-traffic authentication service.
parent 1c931821
No related branches found
No related tags found
No related merge requests found
...@@ -4,15 +4,125 @@ import ( ...@@ -4,15 +4,125 @@ import (
"flag" "flag"
"fmt" "fmt"
"log" "log"
"math/rand"
"time"
"git.autistici.org/ai3/go-common/pwhash" "git.autistici.org/ai3/go-common/pwhash"
) )
var (
algo = flag.String("algo", "argon2", "password hashing algorithm to use")
argon2Time = flag.Int("time", 3, "argon2 `time` parameter")
argon2Mem = flag.Int("mem", 32, "argon2 `memory` parameter (Mb)")
argon2Threads = flag.Int("threads", 4, "argon2 `threads` parameter")
scryptN = flag.Int("n", 16384, "scrypt `n` parameter")
scryptR = flag.Int("r", 8, "scrypt `r` parameter")
scryptP = flag.Int("p", 1, "scrypt `p` parameter")
doBench = flag.Bool("bench", false, "run a benchmark")
doCompare = flag.Bool("compare", false, "compare password against hash")
)
var randSrc = rand.New(rand.NewSource(time.Now().Unix()))
func fillRandomBytes(b []byte, n int) []byte {
for i := 0; i < n; i += 8 {
r := randSrc.Uint64()
b[i] = byte(r & 0xff)
b[i+1] = byte((r >> 8) & 0xff)
b[i+2] = byte((r >> 16) & 0xff)
b[i+3] = byte((r >> 24) & 0xff)
b[i+4] = byte((r >> 32) & 0xff)
b[i+5] = byte((r >> 40) & 0xff)
b[i+6] = byte((r >> 48) & 0xff)
b[i+7] = byte((r >> 56) & 0xff)
}
return b[:n]
}
var pwbuf = make([]byte, 128)
func randomPass() string {
pwlen := 10 + rand.Intn(20)
return string(fillRandomBytes(pwbuf, pwlen))
}
const (
// Run at least these many iterations, then keep going until
// we reach the timeout.
benchChunkSize = 100
// How long to run benchmarks for (more or less).
benchTimeout = 5 * time.Second
)
func runBenchChunk(enc string) int {
pw := randomPass()
for i := 0; i < benchChunkSize; i++ {
pwhash.ComparePassword(enc, pw)
}
return benchChunkSize
}
func runBench(h pwhash.PasswordHash, hname string) {
start := time.Now()
deadline := start.Add(benchTimeout)
enc := h.Encrypt(randomPass())
var n int
for time.Now().Before(deadline) {
n += runBenchChunk(enc)
}
elapsed := time.Since(start)
opsPerSec := float64(n) / elapsed.Seconds()
msPerOp := (elapsed.Seconds() * 1000) / float64(n)
log.Printf("%s: %.4g ops/sec, %.4g ms/op", hname, opsPerSec, msPerOp)
}
func mkhash() (pwhash.PasswordHash, string, error) {
var h pwhash.PasswordHash
name := *algo
switch *algo {
case "argon2":
h = pwhash.NewArgon2WithParams(uint32(*argon2Time), uint32(*argon2Mem*1024), uint8(*argon2Threads))
name = fmt.Sprintf("%s(%d/%d/%d)", *algo, *argon2Time, *argon2Mem, *argon2Threads)
case "scrypt":
h = pwhash.NewScryptWithParams(*scryptN, *scryptR, *scryptP)
name = fmt.Sprintf("%s(%d/%d/%d)", *algo, *scryptN, *scryptR, *scryptP)
case "system":
h = pwhash.NewSystemCrypt()
default:
return nil, "", fmt.Errorf("unknown algo %q", *algo)
}
return h, name, nil
}
func main() { func main() {
log.SetFlags(0)
flag.Parse() flag.Parse()
if flag.NArg() < 1 {
log.Fatal("not enough arguments") h, hname, err := mkhash()
if err != nil {
log.Fatal(err)
} }
fmt.Printf("%s\n", pwhash.Encrypt(flag.Arg(0))) switch {
case *doBench:
runBench(h, hname)
case *doCompare:
if flag.NArg() < 2 {
log.Fatal("not enough arguments")
}
if ok := h.ComparePassword(flag.Arg(0), flag.Arg(1)); !ok {
log.Fatal("password does not match")
}
log.Printf("password ok")
default:
if flag.NArg() < 1 {
log.Fatal("not enough arguments")
}
fmt.Printf("%s\n", h.Encrypt(flag.Arg(0)))
}
} }
...@@ -15,7 +15,8 @@ import ( ...@@ -15,7 +15,8 @@ import (
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
) )
// PasswordHash is a convenience interface common to all types in this package. // PasswordHash is the interface for a password hashing algorithm
// implementation.
type PasswordHash interface { type PasswordHash interface {
// ComparePassword returns true if the given password matches // ComparePassword returns true if the given password matches
// the encrypted one. // the encrypted one.
...@@ -25,12 +26,20 @@ type PasswordHash interface { ...@@ -25,12 +26,20 @@ type PasswordHash interface {
Encrypt(string) string Encrypt(string) string
} }
// SystemCryptPasswordHash uses the glibc crypt function. // systemCryptPasswordHash uses the glibc crypt function.
type SystemCryptPasswordHash struct{} type systemCryptPasswordHash struct {
hashStr string
}
// NewSystemCrypt returns a PasswordHash that uses the system crypt(3)
// function, specifically glibc with its SHA512 algorithm.
func NewSystemCrypt() PasswordHash {
return &systemCryptPasswordHash{"$6$"}
}
// ComparePassword returns true if the given password matches the // ComparePassword returns true if the given password matches the
// encrypted one. // encrypted one.
func (s *SystemCryptPasswordHash) ComparePassword(encrypted, password string) bool { func (s *systemCryptPasswordHash) ComparePassword(encrypted, password string) bool {
enc2, err := crypt.Crypt(password, encrypted) enc2, err := crypt.Crypt(password, encrypted)
if err != nil { if err != nil {
return false return false
...@@ -39,8 +48,8 @@ func (s *SystemCryptPasswordHash) ComparePassword(encrypted, password string) bo ...@@ -39,8 +48,8 @@ func (s *SystemCryptPasswordHash) ComparePassword(encrypted, password string) bo
} }
// Encrypt the given password using glibc crypt. // Encrypt the given password using glibc crypt.
func (s *SystemCryptPasswordHash) Encrypt(password string) string { func (s *systemCryptPasswordHash) Encrypt(password string) string {
salt := fmt.Sprintf("$6$%x$", getRandomBytes(16)) salt := fmt.Sprintf("%s%x$", s.hashStr, getRandomBytes(16))
enc, err := crypt.Crypt(password, salt) enc, err := crypt.Crypt(password, salt)
if err != nil { if err != nil {
panic(err) panic(err)
...@@ -54,28 +63,48 @@ var ( ...@@ -54,28 +63,48 @@ var (
) )
// Argon2PasswordHash uses the Argon2 hashing algorithm. // Argon2PasswordHash uses the Argon2 hashing algorithm.
type Argon2PasswordHash struct{} type argon2PasswordHash struct {
// Encoding parameters.
params argon2Params
}
// NewArgon2 returns an Argon2i-based PasswordHash using the default parameters.
func NewArgon2() PasswordHash {
return NewArgon2WithParams(
defaultArgon2Params.Time,
defaultArgon2Params.Memory,
defaultArgon2Params.Threads,
)
}
// NewArgon2WithParams returns an Argon2i-based PasswordHash using the
// specified parameters for time, memory, and number of threads.
func NewArgon2WithParams(time, mem uint32, threads uint8) PasswordHash {
return &argon2PasswordHash{
params: argon2Params{
Time: time,
Memory: mem,
Threads: threads,
},
}
}
// ComparePassword returns true if the given password matches the // ComparePassword returns true if the given password matches the
// encrypted one. // encrypted one.
func (s *Argon2PasswordHash) ComparePassword(encrypted, password string) bool { func (s *argon2PasswordHash) ComparePassword(encrypted, password string) bool {
params, salt, dk, err := decodeArgon2Hash(encrypted) params, salt, dk, err := decodeArgon2Hash(encrypted)
if err != nil { if err != nil {
return false return false
} }
dk2 := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen) dk2 := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen)
//log.Printf("params=%+v, salt=%+v, dk=%v, dk2=%v", params, salt, dk, dk2)
return subtle.ConstantTimeCompare(dk, dk2) == 1 return subtle.ConstantTimeCompare(dk, dk2) == 1
} }
// Encrypt the given password with the Argon2 algorithm. // Encrypt the given password with the Argon2 algorithm.
func (s *Argon2PasswordHash) Encrypt(password string) string { func (s *argon2PasswordHash) Encrypt(password string) string {
salt := getRandomBytes(argonSaltLen) salt := getRandomBytes(argonSaltLen)
params := defaultArgon2Params dk := argon2.Key([]byte(password), salt, s.params.Time, s.params.Memory, s.params.Threads, argonKeyLen)
return encodeArgon2Hash(s.params, salt, dk)
dk := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen)
return encodeArgon2Hash(params, salt, dk)
} }
type argon2Params struct { type argon2Params struct {
...@@ -84,13 +113,12 @@ type argon2Params struct { ...@@ -84,13 +113,12 @@ type argon2Params struct {
Threads uint8 Threads uint8
} }
// Default Argon2 parameters are tuned for a high-traffic
// authentication service (<1ms per operation).
var defaultArgon2Params = argon2Params{ var defaultArgon2Params = argon2Params{
Time: 4, Time: 1,
Memory: 32 * 1024, Memory: 4 * 1024,
Threads: 1, Threads: 4,
// Test fails with threads > 1 !!
//Threads: uint8(runtime.NumCPU()),
} }
func encodeArgon2Hash(params argon2Params, salt, dk []byte) string { func encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
...@@ -139,11 +167,35 @@ var ( ...@@ -139,11 +167,35 @@ var (
) )
// ScryptPasswordHash uses the scrypt hashing algorithm. // ScryptPasswordHash uses the scrypt hashing algorithm.
type ScryptPasswordHash struct{} type scryptPasswordHash struct {
params scryptParams
}
// NewScrypt returns a PasswordHash that uses the scrypt algorithm
// with the default parameters.
func NewScrypt() PasswordHash {
return NewScryptWithParams(
defaultScryptParams.N,
defaultScryptParams.R,
defaultScryptParams.P,
)
}
// NewScryptWithParams returns a PasswordHash that uses the scrypt
// algorithm with the specified parameters.
func NewScryptWithParams(n, r, p int) PasswordHash {
return &scryptPasswordHash{
params: scryptParams{
N: n,
R: r,
P: p,
},
}
}
// ComparePassword returns true if the given password matches // ComparePassword returns true if the given password matches
// the encrypted one. // the encrypted one.
func (s *ScryptPasswordHash) ComparePassword(encrypted, password string) bool { func (s *scryptPasswordHash) ComparePassword(encrypted, password string) bool {
params, salt, dk, err := decodeScryptHash(encrypted) params, salt, dk, err := decodeScryptHash(encrypted)
if err != nil { if err != nil {
return false return false
...@@ -152,21 +204,19 @@ func (s *ScryptPasswordHash) ComparePassword(encrypted, password string) bool { ...@@ -152,21 +204,19 @@ func (s *ScryptPasswordHash) ComparePassword(encrypted, password string) bool {
if err != nil { if err != nil {
return false return false
} }
//log.Printf("params=%+v, salt=%+v, dk=%v, dk2=%v", params, salt, dk, dk2)
return subtle.ConstantTimeCompare(dk, dk2) == 1 return subtle.ConstantTimeCompare(dk, dk2) == 1
} }
// Encrypt the given password with the scrypt algorithm. // Encrypt the given password with the scrypt algorithm.
func (s *ScryptPasswordHash) Encrypt(password string) string { func (s *scryptPasswordHash) Encrypt(password string) string {
salt := getRandomBytes(scryptSaltLen) salt := getRandomBytes(scryptSaltLen)
params := defaultScryptParams
dk, err := scrypt.Key([]byte(password), salt, params.N, params.R, params.P, scryptKeyLen) dk, err := scrypt.Key([]byte(password), salt, s.params.N, s.params.R, s.params.P, scryptKeyLen)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return encodeScryptHash(params, salt, dk) return encodeScryptHash(s.params, salt, dk)
} }
type scryptParams struct { type scryptParams struct {
...@@ -224,12 +274,13 @@ func getRandomBytes(n int) []byte { ...@@ -224,12 +274,13 @@ func getRandomBytes(n int) []byte {
return b return b
} }
// A registry of default handlers for decoding passwords.
var prefixRegistry = map[string]PasswordHash{ var prefixRegistry = map[string]PasswordHash{
"$1$": &SystemCryptPasswordHash{}, "$1$": NewSystemCrypt(),
"$5$": &SystemCryptPasswordHash{}, "$5$": NewSystemCrypt(),
"$6$": &SystemCryptPasswordHash{}, "$6$": NewSystemCrypt(),
"$s$": &ScryptPasswordHash{}, "$s$": NewScrypt(),
"$a2$": &Argon2PasswordHash{}, "$a2$": NewArgon2(),
} }
// ComparePassword returns true if the given password matches the // ComparePassword returns true if the given password matches the
...@@ -245,7 +296,11 @@ func ComparePassword(encrypted, password string) bool { ...@@ -245,7 +296,11 @@ func ComparePassword(encrypted, password string) bool {
// DefaultEncryptAlgorithm is used by the Encrypt function to encrypt // DefaultEncryptAlgorithm is used by the Encrypt function to encrypt
// passwords. // passwords.
var DefaultEncryptAlgorithm PasswordHash = &Argon2PasswordHash{} var DefaultEncryptAlgorithm PasswordHash
func init() {
DefaultEncryptAlgorithm = NewArgon2()
}
// Encrypt will encrypt a password with the default algorithm. // Encrypt will encrypt a password with the default algorithm.
func Encrypt(password string) string { func Encrypt(password string) string {
......
...@@ -3,15 +3,15 @@ package pwhash ...@@ -3,15 +3,15 @@ package pwhash
import "testing" import "testing"
func TestArgon2(t *testing.T) { func TestArgon2(t *testing.T) {
testImpl(t, &Argon2PasswordHash{}) testImpl(t, NewArgon2())
} }
func TestScrypt(t *testing.T) { func TestScrypt(t *testing.T) {
testImpl(t, &ScryptPasswordHash{}) testImpl(t, NewScrypt())
} }
func TestSystemCrypt(t *testing.T) { func TestSystemCrypt(t *testing.T) {
testImpl(t, &SystemCryptPasswordHash{}) testImpl(t, NewSystemCrypt())
} }
func TestDefault(t *testing.T) { func TestDefault(t *testing.T) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment