diff --git a/pwhash/password.go b/pwhash/password.go new file mode 100644 index 0000000000000000000000000000000000000000..5f3abea1647b26793a766907807aab6f5a60c118 --- /dev/null +++ b/pwhash/password.go @@ -0,0 +1,253 @@ +package pwhash + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/hex" + "errors" + "fmt" + "io" + "strconv" + "strings" + + "github.com/amoghe/go-crypt" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/scrypt" +) + +// PasswordHash is a convenience interface common to all types in this package. +type PasswordHash interface { + // ComparePassword returns true if the given password matches + // the encrypted one. + ComparePassword(string, string) bool + + // Encrypt the given password. + Encrypt(string) string +} + +// SystemCryptPasswordHash uses the glibc crypt function. +type SystemCryptPasswordHash struct{} + +// ComparePassword returns true if the given password matches the +// encrypted one. +func (s *SystemCryptPasswordHash) ComparePassword(encrypted, password string) bool { + enc2, err := crypt.Crypt(password, encrypted) + if err != nil { + return false + } + return subtle.ConstantTimeCompare([]byte(encrypted), []byte(enc2)) == 1 +} + +// Encrypt the given password using glibc crypt. +func (s *SystemCryptPasswordHash) Encrypt(password string) string { + salt := fmt.Sprintf("$6$%x$", getRandomBytes(16)) + enc, err := crypt.Crypt(password, salt) + if err != nil { + panic(err) + } + return enc +} + +var ( + argonKeyLen uint32 = 32 + argonSaltLen = 16 +) + +// Argon2PasswordHash uses the Argon2 hashing algorithm. +type Argon2PasswordHash struct{} + +// ComparePassword returns true if the given password matches the +// encrypted one. +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 { + salt := getRandomBytes(argonSaltLen) + params := defaultArgon2Params + + dk := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen) + + return encodeArgon2Hash(params, salt, dk) +} + +type argon2Params struct { + Time uint32 + Memory uint32 + Threads uint8 +} + +var defaultArgon2Params = argon2Params{ + Time: 4, + Memory: 32 * 1024, + Threads: 1, + + // Test fails with threads > 1 !! + //Threads: uint8(runtime.NumCPU()), +} + +func encodeArgon2Hash(params argon2Params, salt, dk []byte) string { + return fmt.Sprintf("$a2$%d$%d$%d$%x$%x", params.Time, params.Memory, params.Threads, salt, dk) +} + +func decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) { + if !strings.HasPrefix(s, "$a2$") { + err = errors.New("not an Argon2 password hash") + return + } + + parts := strings.SplitN(s[4:], "$", 5) + if len(parts) != 5 { + err = errors.New("bad encoding") + return + } + + var i uint64 + + if i, err = strconv.ParseUint(parts[0], 10, 32); err != nil { + return + } + params.Time = uint32(i) + + if i, err = strconv.ParseUint(parts[1], 10, 32); err != nil { + return + } + params.Memory = uint32(i) + + if i, err = strconv.ParseUint(parts[2], 10, 8); err != nil { + return + } + params.Threads = uint8(i) + + if salt, err = hex.DecodeString(parts[3]); err != nil { + return + } + dk, err = hex.DecodeString(parts[4]) + return +} + +var ( + scryptKeyLen = 32 + scryptSaltLen = 16 +) + +// ScryptPasswordHash uses the scrypt hashing algorithm. +type ScryptPasswordHash struct{} + +// ComparePassword returns true if the given password matches +// the encrypted one. +func (s *ScryptPasswordHash) ComparePassword(encrypted, password string) bool { + params, salt, dk, err := decodeScryptHash(encrypted) + if err != nil { + return false + } + dk2, err := scrypt.Key([]byte(password), salt, params.N, params.R, params.P, scryptKeyLen) + 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 { + salt := getRandomBytes(scryptSaltLen) + params := defaultScryptParams + + dk, err := scrypt.Key([]byte(password), salt, params.N, params.R, params.P, scryptKeyLen) + if err != nil { + panic(err) + } + + return encodeScryptHash(params, salt, dk) +} + +type scryptParams struct { + N int + R int + P int +} + +var defaultScryptParams = scryptParams{ + N: 16384, + R: 8, + P: 1, +} + +func encodeScryptHash(params scryptParams, salt, dk []byte) string { + return fmt.Sprintf("$s$%d$%d$%d$%x$%x", params.N, params.R, params.P, salt, dk) +} + +func decodeScryptHash(s string) (params scryptParams, salt []byte, dk []byte, err error) { + if !strings.HasPrefix(s, "$s$") { + err = errors.New("not a scrypt password hash") + return + } + + parts := strings.SplitN(s[3:], "$", 5) + if len(parts) != 5 { + err = errors.New("bad encoding") + return + } + + if params.N, err = strconv.Atoi(parts[0]); err != nil { + return + } + + if params.R, err = strconv.Atoi(parts[1]); err != nil { + return + } + if params.P, err = strconv.Atoi(parts[2]); err != nil { + return + } + + if salt, err = hex.DecodeString(parts[3]); err != nil { + return + } + dk, err = hex.DecodeString(parts[4]) + return +} + +func getRandomBytes(n int) []byte { + b := make([]byte, n) + _, err := io.ReadFull(rand.Reader, b[:]) + if err != nil { + panic(err) + } + return b +} + +var prefixRegistry = map[string]PasswordHash{ + "$1$": &SystemCryptPasswordHash{}, + "$5$": &SystemCryptPasswordHash{}, + "$6$": &SystemCryptPasswordHash{}, + "$s$": &ScryptPasswordHash{}, + "$a2$": &Argon2PasswordHash{}, +} + +// ComparePassword returns true if the given password matches the +// encrypted one. +func ComparePassword(encrypted, password string) bool { + for pfx, h := range prefixRegistry { + if strings.HasPrefix(encrypted, pfx) { + return h.ComparePassword(encrypted, password) + } + } + return false +} + +// DefaultEncryptAlgorithm is used by the Encrypt function to encrypt +// passwords. +var DefaultEncryptAlgorithm PasswordHash = &Argon2PasswordHash{} + +// Encrypt will encrypt a password with the default algorithm. +func Encrypt(password string) string { + return DefaultEncryptAlgorithm.Encrypt(password) +} diff --git a/pwhash/password_test.go b/pwhash/password_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0908962167f5fc076bf1aa13889d89e3628b75b4 --- /dev/null +++ b/pwhash/password_test.go @@ -0,0 +1,59 @@ +package pwhash + +import "testing" + +func TestArgon2(t *testing.T) { + testImpl(t, &Argon2PasswordHash{}) +} + +func TestScrypt(t *testing.T) { + testImpl(t, &ScryptPasswordHash{}) +} + +func TestSystemCrypt(t *testing.T) { + testImpl(t, &SystemCryptPasswordHash{}) +} + +func TestDefault(t *testing.T) { + testImpl(t, nil) +} + +func testImpl(t *testing.T, h PasswordHash) { + pw1 := "password 1" + pw2 := "password 2" + + var enc1, enc2 string + if h == nil { + enc1 = Encrypt(pw1) + enc2 = Encrypt(pw2) + } else { + enc1 = h.Encrypt(pw1) + enc2 = h.Encrypt(pw2) + } + //t.Logf("enc1=%s", enc1) + + testData := []struct { + enc string + pw string + expectedResult bool + }{ + {enc1, pw1, true}, + {enc2, pw2, true}, + {enc1, pw2, false}, + {enc2, pw1, false}, + {enc1, "", false}, + {enc1, "foo", false}, + } + + for _, td := range testData { + var result bool + if h == nil { + result = ComparePassword(td.enc, td.pw) + } else { + result = h.ComparePassword(td.enc, td.pw) + } + if result != td.expectedResult { + t.Errorf("compare(%s, %s): got %v, expected %v", td.enc, td.pw, result, td.expectedResult) + } + } +}