Commit 3e5c304b authored by ale's avatar ale

Add the standard Argon2 password encoding format

Refactor the pwhash package to split the various encoders into their
own files for readability.
parent 13036824
Pipeline #7568 passed with stage
in 59 seconds
package pwhash
import (
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/argon2"
)
var (
argonKeyLen uint32 = 32
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.
func newArgon2PasswordHash(time, mem uint32, threads uint8, codec argon2Codec) PasswordHash {
return &argon2PasswordHash{
params: argon2Params{
Time: time,
Memory: mem,
Threads: threads,
},
codec: codec,
}
}
// 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 newArgon2PasswordHash(time, mem, threads, &a2Codec{})
}
// NewArgon2StdWithParams returns an Argon2i-based PasswordHash using
// the specified parameters for time, memory, and number of
// threads. This will use the string encoding ("$argon2$") documented
// in the argon2 reference implementation.
func NewArgon2StdWithParams(time, mem uint32, threads uint8) PasswordHash {
return newArgon2PasswordHash(time, mem, threads, &argon2StdCodec{})
}
// 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
}
dk2 := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen)
return subtle.ConstantTimeCompare(dk, dk2) == 1
}
// Encrypt the given password with the Argon2 algorithm.
func (s *argon2PasswordHash) Encrypt(password string) string {
salt := getRandomBytes(argonSaltLen)
dk := argon2.Key([]byte(password), salt, s.params.Time, s.params.Memory, s.params.Threads, argonKeyLen)
return s.codec.encodeArgon2Hash(s.params, salt, dk)
}
type argon2Params struct {
Time uint32
Memory uint32
Threads uint8
}
// Default Argon2 parameters are tuned for a high-traffic
// authentication service (<1ms per operation).
var defaultArgon2Params = argon2Params{
Time: 1,
Memory: 4 * 1024,
Threads: 4,
}
type argon2Codec interface {
encodeArgon2Hash(argon2Params, []byte, []byte) string
decodeArgon2Hash(string) (argon2Params, []byte, []byte, error)
}
type a2Codec struct{}
func (*a2Codec) 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 (*a2Codec) 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)
salt, err = hex.DecodeString(parts[3])
if err != nil {
return
}
dk, err = hex.DecodeString(parts[4])
return
}
type argon2StdCodec struct{}
func (*argon2StdCodec) encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
encSalt := base64.RawStdEncoding.EncodeToString(salt)
encDK := base64.RawStdEncoding.EncodeToString(dk)
return fmt.Sprintf("$argon2i$v=19$m=%d,t=%d,p=%d$%s$%s", params.Memory, params.Time, params.Threads, encSalt, encDK)
}
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:
err = errors.New("unknown parameter in hash")
}
if err != nil {
return
}
}
return
}
func (*argon2StdCodec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) {
if !strings.HasPrefix(s, "$argon2i$") {
err = errors.New("not an Argon2 password hash")
return
}
parts := strings.SplitN(s[9:], "$", 4)
if len(parts) != 4 {
err = errors.New("bad encoding")
return
}
if parts[0] != "v=19" {
err = errors.New("bad argon2 hash version")
return
}
params, err = parseArgon2HashParams(parts[1])
if err != nil {
return
}
if salt, err = base64.RawStdEncoding.DecodeString(parts[2]); err != nil {
return
}
dk, err = base64.RawStdEncoding.DecodeString(parts[3])
return
}
package pwhash
import (
"crypto/subtle"
"fmt"
"github.com/amoghe/go-crypt"
)
// 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 {
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("%s%x$", s.hashStr, getRandomBytes(16))
enc, err := crypt.Crypt(password, salt)
if err != nil {
panic(err)
}
return enc
}
......@@ -23,17 +23,8 @@ 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 the interface for a password hashing algorithm
......@@ -47,245 +38,6 @@ type PasswordHash interface {
Encrypt(string) string
}
// 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 {
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("%s%x$", s.hashStr, 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 {
// 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 {
params, salt, dk, err := decodeArgon2Hash(encrypted)
if err != nil {
return false
}
dk2 := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen)
return subtle.ConstantTimeCompare(dk, dk2) == 1
}
// Encrypt the given password with the Argon2 algorithm.
func (s *argon2PasswordHash) Encrypt(password string) string {
salt := getRandomBytes(argonSaltLen)
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 {
Time uint32
Memory uint32
Threads uint8
}
// Default Argon2 parameters are tuned for a high-traffic
// authentication service (<1ms per operation).
var defaultArgon2Params = argon2Params{
Time: 1,
Memory: 4 * 1024,
Threads: 4,
}
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 {
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 {
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
}
return subtle.ConstantTimeCompare(dk, dk2) == 1
}
// Encrypt the given password with the scrypt algorithm.
func (s *scryptPasswordHash) Encrypt(password string) string {
salt := getRandomBytes(scryptSaltLen)
dk, err := scrypt.Key([]byte(password), salt, s.params.N, s.params.R, s.params.P, scryptKeyLen)
if err != nil {
panic(err)
}
return encodeScryptHash(s.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[:])
......
......@@ -9,6 +9,10 @@ func TestArgon2(t *testing.T) {
testImpl(t, NewArgon2())
}
func TestArgon2Std(t *testing.T) {
testImpl(t, NewArgon2StdWithParams(3, 4096, 2))
}
func TestScrypt(t *testing.T) {
testImpl(t, NewScrypt())
}
......
package pwhash
import (
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/scrypt"
)
var (
scryptKeyLen = 32
scryptSaltLen = 16
)
// ScryptPasswordHash uses the scrypt hashing algorithm.
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 {
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
}
return subtle.ConstantTimeCompare(dk, dk2) == 1
}
// Encrypt the given password with the scrypt algorithm.
func (s *scryptPasswordHash) Encrypt(password string) string {
salt := getRandomBytes(scryptSaltLen)
dk, err := scrypt.Key([]byte(password), salt, s.params.N, s.params.R, s.params.P, scryptKeyLen)
if err != nil {
panic(err)
}
return encodeScryptHash(s.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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment