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) {