Skip to content
Snippets Groups Projects
argon2.go 6.96 KiB
Newer Older
ale's avatar
ale committed
package pwhash

import (
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"golang.org/x/crypto/argon2"
)

ale's avatar
ale committed
const (
	argonLegacyKeySize  = 32
	argonDefaultKeySize = 16
ale's avatar
ale committed
	argonSaltLen        = 16
)

// Argon2PasswordHash uses the Argon2 hashing algorithm.
type argon2PasswordHash struct {
	// Encoding parameters.
	params argon2Params

	// Codec for string encoding.
	codec argon2Codec
}

// newArgon2PasswordHash returns an Argon2i-based PasswordHash using the
// specified parameters for time, memory, and number of threads.
ale's avatar
ale committed
func newArgon2PasswordHash(kind string, keySize int, time, mem uint32, threads uint8, codec argon2Codec) PasswordHash {
ale's avatar
ale committed
	return &argon2PasswordHash{
		params: argon2Params{
ale's avatar
ale committed
			KeySize: keySize,
			Kind:    kind,
ale's avatar
ale committed
			Time:    time,
			Memory:  mem,
			Threads: threads,
		},
		codec: codec,
	}
}

// NewArgon2 returns an Argon2i-based PasswordHash using the default parameters.
ale's avatar
ale committed
func NewArgon2Legacy() PasswordHash {
	return NewArgon2LegacyWithParams(
ale's avatar
ale committed
		defaultArgon2Params.Time,
		defaultArgon2Params.Memory,
		defaultArgon2Params.Threads,
	)
}

// NewArgon2WithParams returns an Argon2i-based PasswordHash using the
// specified parameters for time, memory, and number of threads.
ale's avatar
ale committed
func NewArgon2LegacyWithParams(time, mem uint32, threads uint8) PasswordHash {
	return newArgon2PasswordHash(kindArgon2I, argonLegacyKeySize, time, mem, threads, &a2LegacyCodec{})
ale's avatar
ale committed
}

// NewArgon2Std returns an Argon2i-based PasswordHash that conforms
// to the reference implementation encoding, using default parameters.
func NewArgon2Std() PasswordHash {
	return NewArgon2StdWithParams(
		defaultArgon2Params.Time,
		defaultArgon2Params.Memory,
		defaultArgon2Params.Threads,
	)
}

ale's avatar
ale committed
// NewArgon2StdWithParams returns an Argon2id-based PasswordHash using
ale's avatar
ale committed
// the specified parameters for time, memory, and number of
ale's avatar
ale committed
// threads. This will use the string encoding ("$argon2id$") documented
ale's avatar
ale committed
// in the argon2 reference implementation.
func NewArgon2StdWithParams(time, mem uint32, threads uint8) PasswordHash {
ale's avatar
ale committed
	return newArgon2PasswordHash(kindArgon2ID, argonDefaultKeySize, time, mem, threads, &argon2StdCodec{})
ale's avatar
ale committed
}

// ComparePassword returns true if the given password matches the
// encrypted one.
func (s *argon2PasswordHash) ComparePassword(encrypted, password string) bool {
	params, salt, dk, err := s.codec.decodeArgon2Hash(encrypted)
	if err != nil {
		return false
	}
ale's avatar
ale committed

	dk2 := params.hash(password, salt)
ale's avatar
ale committed
	return subtle.ConstantTimeCompare(dk, dk2) == 1
}

// Encrypt the given password with the Argon2 algorithm.
func (s *argon2PasswordHash) Encrypt(password string) string {
	salt := getRandomBytes(argonSaltLen)
ale's avatar
ale committed
	dk := s.params.hash(password, salt)
ale's avatar
ale committed
	return s.codec.encodeArgon2Hash(s.params, salt, dk)
}

ale's avatar
ale committed
const (
	kindArgon2I  = "argon2i"
	kindArgon2ID = "argon2id"
)

ale's avatar
ale committed
type argon2Params struct {
ale's avatar
ale committed
	Kind    string
	KeySize int
ale's avatar
ale committed
	Time    uint32
	Memory  uint32
	Threads uint8
}

ale's avatar
ale committed
func (p argon2Params) hash(password string, salt []byte) []byte {
	if p.KeySize == 0 {
		panic("key size is 0")
	}

	switch p.Kind {
	case kindArgon2I:
		return argon2.Key([]byte(password), salt, p.Time, p.Memory, p.Threads, uint32(p.KeySize))
	case kindArgon2ID:
		return argon2.IDKey([]byte(password), salt, p.Time, p.Memory, p.Threads, uint32(p.KeySize))
	default:
		panic("unknown argon2 hash kind")
	}
}

ale's avatar
ale committed
// Default Argon2 parameters are tuned for a high-traffic
// authentication service (<1ms per operation).
var defaultArgon2Params = argon2Params{
ale's avatar
ale committed
	Kind:    kindArgon2ID,
	KeySize: 16,
ale's avatar
ale committed
	Time:    1,
ale's avatar
ale committed
	Memory:  64 * 1024,
ale's avatar
ale committed
	Threads: 4,
}

type argon2Codec interface {
	encodeArgon2Hash(argon2Params, []byte, []byte) string
	decodeArgon2Hash(string) (argon2Params, []byte, []byte, error)
}

ale's avatar
ale committed
// Argon2i legacy encoding, do not use.
type a2LegacyCodec struct{}
ale's avatar
ale committed

ale's avatar
ale committed
func (*a2LegacyCodec) encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
ale's avatar
ale committed
	return fmt.Sprintf("$a2$%d$%d$%d$%x$%x", params.Time, params.Memory, params.Threads, salt, dk)
}

ale's avatar
ale committed
func (*a2LegacyCodec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) {
ale's avatar
ale committed
	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
	}

ale's avatar
ale committed
	params.Kind = kindArgon2I

ale's avatar
ale committed
	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)

	salt, err = hex.DecodeString(parts[3])
	if err != nil {
		return
	}
ale's avatar
ale committed

ale's avatar
ale committed
	dk, err = hex.DecodeString(parts[4])
ale's avatar
ale committed
	if err != nil {
		return
	}

	params.KeySize = len(dk)
	switch len(dk) {
	case 16, 24, 32:
	default:
		err = errors.New("bad key size")
	}

ale's avatar
ale committed
	return
}

ale's avatar
ale committed
// Standard Argon2 encoding as per the reference implementation in
// https://github.com/P-H-C/phc-winner-argon2/blob/4ac8640c2adc1257677d27d3f833c8d1ee68c7d2/src/encoding.c#L242-L252
ale's avatar
ale committed
type argon2StdCodec struct{}

ale's avatar
ale committed
const argon2HashVersionStr = "v=19"

ale's avatar
ale committed
func (*argon2StdCodec) encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
	encSalt := base64.RawStdEncoding.EncodeToString(salt)
	encDK := base64.RawStdEncoding.EncodeToString(dk)
ale's avatar
ale committed
	return fmt.Sprintf(
		"$%s$%s$m=%d,t=%d,p=%d$%s$%s",
		params.Kind, argon2HashVersionStr,
		params.Memory, params.Time, params.Threads,
		encSalt, encDK)
ale's avatar
ale committed
}

func parseArgon2HashParams(s string) (params argon2Params, err error) {
	params = defaultArgon2Params
	parts := strings.Split(s, ",")
	for _, ss := range parts {
		kv := strings.SplitN(ss, "=", 2)
		if len(kv) != 2 {
			err = errors.New("bad parameter encoding")
			return
		}
		var i uint64
		switch kv[0] {
		case "t":
			i, err = strconv.ParseUint(kv[1], 10, 32)
			params.Time = uint32(i)
		case "m":
			i, err = strconv.ParseUint(kv[1], 10, 32)
			params.Memory = uint32(i)
		case "p":
			i, err = strconv.ParseUint(kv[1], 10, 8)
			params.Threads = uint8(i)
		default:
ale's avatar
ale committed
			err = fmt.Errorf("unknown parameter '%s' in hash", kv[0])
ale's avatar
ale committed
		}
		if err != nil {
			return
		}
	}
	return
}

func (*argon2StdCodec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) {
ale's avatar
ale committed
	var kind string
	switch {
	case strings.HasPrefix(s, "$argon2i$"):
		kind = kindArgon2I
	case strings.HasPrefix(s, "$argon2id$"):
		kind = kindArgon2ID
	default:
ale's avatar
ale committed
		err = errors.New("not an Argon2 password hash")
		return
	}

ale's avatar
ale committed
	parts := strings.SplitN(s, "$", 6)
	if len(parts) != 6 {
ale's avatar
ale committed
		err = errors.New("bad encoding")
		return
	}
ale's avatar
ale committed
	if parts[2] != argon2HashVersionStr {
ale's avatar
ale committed
		err = errors.New("bad argon2 hash version")
		return
	}

ale's avatar
ale committed
	params, err = parseArgon2HashParams(parts[3])
ale's avatar
ale committed
	if err != nil {
		return
	}
ale's avatar
ale committed
	params.Kind = kind

	if salt, err = base64.RawStdEncoding.DecodeString(parts[4]); err != nil {
		return
	}
	if dk, err = base64.RawStdEncoding.DecodeString(parts[5]); err != nil {
ale's avatar
ale committed
		return
	}

ale's avatar
ale committed
	params.KeySize = len(dk)
	switch len(dk) {
	case 16, 24, 32:
	default:
		err = errors.New("bad key size")
	}

ale's avatar
ale committed
	return
}