diff options
Diffstat (limited to 'forged/internal/argon2id/argon2id.go')
-rw-r--r-- | forged/internal/argon2id/argon2id.go | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/forged/internal/argon2id/argon2id.go b/forged/internal/argon2id/argon2id.go new file mode 100644 index 0000000..88df8f6 --- /dev/null +++ b/forged/internal/argon2id/argon2id.go @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (c) 2018 Alex Edwards + +// Package argon2id provides a wrapper around Go's golang.org/x/crypto/argon2. +package argon2id + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "runtime" + "strings" + + "golang.org/x/crypto/argon2" +) + +var ( + // ErrInvalidHash in returned by ComparePasswordAndHash if the provided + // hash isn't in the expected format. + ErrInvalidHash = errors.New("argon2id: hash is not in the correct format") + + // ErrIncompatibleVariant is returned by ComparePasswordAndHash if the + // provided hash was created using a unsupported variant of Argon2. + // Currently only argon2id is supported by this package. + ErrIncompatibleVariant = errors.New("argon2id: incompatible variant of argon2") + + // ErrIncompatibleVersion is returned by ComparePasswordAndHash if the + // provided hash was created using a different version of Argon2. + ErrIncompatibleVersion = errors.New("argon2id: incompatible version of argon2") +) + +// DefaultParams provides some sane default parameters for hashing passwords. +// +// Follows recommendations given by the Argon2 RFC: +// "The Argon2id variant with t=1 and maximum available memory is RECOMMENDED as a +// default setting for all environments. This setting is secure against side-channel +// attacks and maximizes adversarial costs on dedicated bruteforce hardware."" +// +// The default parameters should generally be used for development/testing purposes +// only. Custom parameters should be set for production applications depending on +// available memory/CPU resources and business requirements. +var DefaultParams = &Params{ + Memory: 64 * 1024, + Iterations: 1, + Parallelism: uint8(runtime.NumCPU()), + SaltLength: 16, + KeyLength: 32, +} + +// Params describes the input parameters used by the Argon2id algorithm. The +// Memory and Iterations parameters control the computational cost of hashing +// the password. The higher these figures are, the greater the cost of generating +// the hash and the longer the runtime. It also follows that the greater the cost +// will be for any attacker trying to guess the password. If the code is running +// on a machine with multiple cores, then you can decrease the runtime without +// reducing the cost by increasing the Parallelism parameter. This controls the +// number of threads that the work is spread across. Important note: Changing the +// value of the Parallelism parameter changes the hash output. +// +// For guidance and an outline process for choosing appropriate parameters see +// https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4 +type Params struct { + // The amount of memory used by the algorithm (in kibibytes). + Memory uint32 + + // The number of iterations over the memory. + Iterations uint32 + + // The number of threads (or lanes) used by the algorithm. + // Recommended value is between 1 and runtime.NumCPU(). + Parallelism uint8 + + // Length of the random salt. 16 bytes is recommended for password hashing. + SaltLength uint32 + + // Length of the generated key. 16 bytes or more is recommended. + KeyLength uint32 +} + +// CreateHash returns an Argon2id hash of a plain-text password using the +// provided algorithm parameters. The returned hash follows the format used by +// the Argon2 reference C implementation and contains the base64-encoded Argon2id d +// derived key prefixed by the salt and parameters. It looks like this: +// +// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG +func CreateHash(password string, params *Params) (hash string, err error) { + salt, err := generateRandomBytes(params.SaltLength) + if err != nil { + return "", err + } + + key := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength) + + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Key := base64.RawStdEncoding.EncodeToString(key) + + hash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64Salt, b64Key) + return hash, nil +} + +// ComparePasswordAndHash performs a constant-time comparison between a +// plain-text password and Argon2id hash, using the parameters and salt +// contained in the hash. It returns true if they match, otherwise it returns +// false. +func ComparePasswordAndHash(password, hash string) (match bool, err error) { + match, _, err = CheckHash(password, hash) + return match, err +} + +// CheckHash is like ComparePasswordAndHash, except it also returns the params that the hash was +// created with. This can be useful if you want to update your hash params over time (which you +// should). +func CheckHash(password, hash string) (match bool, params *Params, err error) { + params, salt, key, err := DecodeHash(hash) + if err != nil { + return false, nil, err + } + + otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength) + + keyLen := int32(len(key)) + otherKeyLen := int32(len(otherKey)) + + if subtle.ConstantTimeEq(keyLen, otherKeyLen) == 0 { + return false, params, nil + } + if subtle.ConstantTimeCompare(key, otherKey) == 1 { + return true, params, nil + } + return false, params, nil +} + +func generateRandomBytes(n uint32) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} + +// DecodeHash expects a hash created from this package, and parses it to return the params used to +// create it, as well as the salt and key (password hash). +func DecodeHash(hash string) (params *Params, salt, key []byte, err error) { + vals := strings.Split(hash, "$") + if len(vals) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + if vals[1] != "argon2id" { + return nil, nil, nil, ErrIncompatibleVariant + } + + var version int + _, err = fmt.Sscanf(vals[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + params = &Params{} + _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4]) + if err != nil { + return nil, nil, nil, err + } + params.SaltLength = uint32(len(salt)) + + key, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) + if err != nil { + return nil, nil, nil, err + } + params.KeyLength = uint32(len(key)) + + return params, salt, key, nil +} |