// Package argon2id provides a convience wrapper around Go's golang.org/x/crypto/argon2 // implementation, making it simpler to securely hash and verify passwords // using Argon2. // // It enforces use of the Argon2id algorithm variant and cryptographically-secure // random salts. 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 a 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 }