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
Branches
No related tags found
No related merge requests found
......@@ -4,15 +4,125 @@ import (
"flag"
"fmt"
"log"
"math/rand"
"time"
"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() {
log.SetFlags(0)
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 (
"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 {
// ComparePassword returns true if the given password matches
// the encrypted one.
......@@ -25,12 +26,20 @@ type PasswordHash interface {
Encrypt(string) string
}
// SystemCryptPasswordHash uses the glibc crypt function.
type SystemCryptPasswordHash struct{}
// systemCryptPasswordHash uses the glibc crypt function.
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
// encrypted one.
func (s *SystemCryptPasswordHash) ComparePassword(encrypted, password string) bool {
func (s *systemCryptPasswordHash) ComparePassword(encrypted, password string) bool {
enc2, err := crypt.Crypt(password, encrypted)
if err != nil {
return false
......@@ -39,8 +48,8 @@ func (s *SystemCryptPasswordHash) ComparePassword(encrypted, password string) bo
}
// Encrypt the given password using glibc crypt.
func (s *SystemCryptPasswordHash) Encrypt(password string) string {
salt := fmt.Sprintf("$6$%x$", getRandomBytes(16))
func (s *systemCryptPasswordHash) Encrypt(password string) string {
salt := fmt.Sprintf("%s%x$", s.hashStr, getRandomBytes(16))
enc, err := crypt.Crypt(password, salt)
if err != nil {
panic(err)
......@@ -54,28 +63,48 @@ var (
)
// 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
// encrypted one.
func (s *Argon2PasswordHash) ComparePassword(encrypted, password string) bool {
func (s *argon2PasswordHash) ComparePassword(encrypted, password string) bool {
params, salt, dk, err := decodeArgon2Hash(encrypted)
if err != nil {
return false
}
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
}
// 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)
params := defaultArgon2Params
dk := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen)
return encodeArgon2Hash(params, salt, dk)
dk := argon2.Key([]byte(password), salt, s.params.Time, s.params.Memory, s.params.Threads, argonKeyLen)
return encodeArgon2Hash(s.params, salt, dk)
}
type argon2Params struct {
......@@ -84,13 +113,12 @@ type argon2Params struct {
Threads uint8
}
// Default Argon2 parameters are tuned for a high-traffic
// authentication service (<1ms per operation).
var defaultArgon2Params = argon2Params{
Time: 4,
Memory: 32 * 1024,
Threads: 1,
// Test fails with threads > 1 !!
//Threads: uint8(runtime.NumCPU()),
Time: 1,
Memory: 4 * 1024,
Threads: 4,
}
func encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
......@@ -139,11 +167,35 @@ var (
)
// 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
// 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)
if err != nil {
return false
......@@ -152,21 +204,19 @@ func (s *ScryptPasswordHash) ComparePassword(encrypted, password string) bool {
if err != nil {
return false
}
//log.Printf("params=%+v, salt=%+v, dk=%v, dk2=%v", params, salt, dk, dk2)
return subtle.ConstantTimeCompare(dk, dk2) == 1
}
// 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)
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 {
panic(err)
}
return encodeScryptHash(params, salt, dk)
return encodeScryptHash(s.params, salt, dk)
}
type scryptParams struct {
......@@ -224,12 +274,13 @@ func getRandomBytes(n int) []byte {
return b
}
// A registry of default handlers for decoding passwords.
var prefixRegistry = map[string]PasswordHash{
"$1$": &SystemCryptPasswordHash{},
"$5$": &SystemCryptPasswordHash{},
"$6$": &SystemCryptPasswordHash{},
"$s$": &ScryptPasswordHash{},
"$a2$": &Argon2PasswordHash{},
"$1$": NewSystemCrypt(),
"$5$": NewSystemCrypt(),
"$6$": NewSystemCrypt(),
"$s$": NewScrypt(),
"$a2$": NewArgon2(),
}
// ComparePassword returns true if the given password matches the
......@@ -245,7 +296,11 @@ func ComparePassword(encrypted, password string) bool {
// DefaultEncryptAlgorithm is used by the Encrypt function to encrypt
// passwords.
var DefaultEncryptAlgorithm PasswordHash = &Argon2PasswordHash{}
var DefaultEncryptAlgorithm PasswordHash
func init() {
DefaultEncryptAlgorithm = NewArgon2()
}
// Encrypt will encrypt a password with the default algorithm.
func Encrypt(password string) string {
......
......@@ -3,15 +3,15 @@ package pwhash
import "testing"
func TestArgon2(t *testing.T) {
testImpl(t, &Argon2PasswordHash{})
testImpl(t, NewArgon2())
}
func TestScrypt(t *testing.T) {
testImpl(t, &ScryptPasswordHash{})
testImpl(t, NewScrypt())
}
func TestSystemCrypt(t *testing.T) {
testImpl(t, &SystemCryptPasswordHash{})
testImpl(t, NewSystemCrypt())
}
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