package argon2id import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" ) var ( // ErrInvalidHash is returned if the required parameters can not be obtained // from the hash. ErrInvalidHash = fmt.Errorf("argon2id: invalid hash format") // ErrVersionIncompatible is returned if the Argon2id version generating // the hash does not match the version validating it. ErrVersionIncompatible = fmt.Errorf("argon2id: incompatible version") // Prefix is set to be compatible with Dovecot. Can be set to an empty string. Prefix = "{ARGON2ID}" ) // DefaultParams provides sane default parameters for password hashing as of // 2021. Depending on your environment you will need to adjust these. var DefaultParams = &Params{ Memory: 512 * 1024, Iterations: 3, Parallelism: 1, // changed from 4 to 1 SaltLen: 32, KeyLen: 32, } // Params contains the input parameters for the Argon2id algorithm. Memory and // Iterations tweak the computational cost. If you have more cores available // you can change the parallelism to reduce runtime without reducing cost. But // note that this will change the hash. // // See https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4 type Params struct { Memory uint32 Iterations uint32 Parallelism uint8 SaltLen uint32 KeyLen uint32 } // Generate generates a new Argon2ID hash with recommended values for it's // complexity parameters. By default the generated hash is compatible with // the Dovecot Password Scheme. // // See https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ // // It looks like this // // {ARGON2ID}$argon2id$v=19$m=524288,t=3,p=4$464unwkIcBGXjqWBZ0A5FWClURgYdWFqRlQaBJOE5fs$5ofdht4OkXsg/tftXGgxNchAdgHzpe+QJyizabiKZFk func Generate(password string, saltLen uint32) (string, []byte, error) { params := DefaultParams if saltLen > 0 { params.SaltLen = saltLen } salt := make([]byte, params.SaltLen) if _, err := rand.Read(salt); err != nil { return "", nil, fmt.Errorf("failed to read rand: %w", err) } hash := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen) b64hash := base64.RawStdEncoding.EncodeToString(hash) b64salt := base64.RawStdEncoding.EncodeToString(salt) return fmt.Sprintf(Prefix+"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64salt, b64hash), hash, nil } // Validate unpacks the parameters from the hash, computes the hash of the given // password with these parameters and performs a constant time comparison between // both hashes. func Validate(password string, hash string) (bool, error) { params, salt, key, err := unpackHash(hash) if err != nil { return false, err } otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLen) if subtle.ConstantTimeEq(int32(len(key)), int32(len(otherKey))) == 0 { return false, nil } if subtle.ConstantTimeCompare(key, otherKey) == 1 { return true, nil } return false, nil } func unpackHash(hash string) (*Params, []byte, []byte, error) { hash = strings.TrimPrefix(hash, Prefix) p := strings.Split(hash, "$") if len(p) != 6 { return nil, nil, nil, ErrInvalidHash } if p[1] != "argon2id" { return nil, nil, nil, ErrInvalidHash } var version int _, err := fmt.Sscanf(p[2], "v=%d", &version) if err != nil { return nil, nil, nil, fmt.Errorf("failed to scan version %s: %w", p[2], err) } if version != argon2.Version { return nil, nil, nil, ErrVersionIncompatible } params := &Params{} _, err = fmt.Sscanf(p[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism) if err != nil { return nil, nil, nil, fmt.Errorf("failed to scan header %s: %w", p[3], err) } salt, err := base64.RawStdEncoding.DecodeString(p[4]) if err != nil { return nil, nil, nil, fmt.Errorf("failed to decode salt %s: %w", p[4], err) } params.SaltLen = uint32(len(salt)) key, err := base64.RawStdEncoding.DecodeString(p[5]) if err != nil { return nil, nil, nil, fmt.Errorf("failed to decode hash %s: %w", p[5], err) } params.KeyLen = uint32(len(key)) return params, salt, key, nil }