diff --git a/cmd/pwtool/main.go b/cmd/pwtool/main.go index 088f6fa757dc51a34125fa41a6a09a555dc85834..7f3a5a08fcb112e14452e9bb96a03c295c986105 100644 --- a/cmd/pwtool/main.go +++ b/cmd/pwtool/main.go @@ -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))) + } } diff --git a/pwhash/password.go b/pwhash/password.go index 5f3abea1647b26793a766907807aab6f5a60c118..f8f84e36a180e2d6634cf0cb7eb0071a548d5f6c 100644 --- a/pwhash/password.go +++ b/pwhash/password.go @@ -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 { diff --git a/pwhash/password_test.go b/pwhash/password_test.go index 0908962167f5fc076bf1aa13889d89e3628b75b4..27ef8f4e23d28f90c4baf17ff674a153e6ae8c98 100644 --- a/pwhash/password_test.go +++ b/pwhash/password_test.go @@ -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) {