Skip to content
Snippets Groups Projects
Commit 7eb77848 authored by ale's avatar ale
Browse files

Add generic password hashing/comparison package

Supports multiple algorithms:
* system crypt
* scrypt
* argon2

We just made up new $something$ prefixes for the new algorithms.
parent aa880113
Branches
No related tags found
No related merge requests found
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)
}
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)
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment