git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/crypto/password.go (about)

     1  package crypto
     2  
     3  // Package argon2id provides a convience wrapper around Go's golang.org/x/crypto/argon2
     4  // implementation, making it simpler to securely hash and verify passwords
     5  // using Argon2.
     6  //
     7  // It enforces use of the Argon2id algorithm variant and cryptographically-secure
     8  // random salts.
     9  
    10  import (
    11  	"crypto/subtle"
    12  	"encoding/base64"
    13  	"errors"
    14  	"fmt"
    15  	"strings"
    16  
    17  	"golang.org/x/crypto/argon2"
    18  )
    19  
    20  var (
    21  	// ErrInvalidPasswordHash in returned by ComparePasswordAndHash if the provided
    22  	// hash isn't in the expected format.
    23  	ErrInvalidPasswordHash = errors.New("crypto: hash is not in the correct format")
    24  
    25  	// ErrIncompatiblePasswordHashVersion in returned by ComparePasswordAndHash if the
    26  	// provided hash was created using a different version of Argon2.
    27  	ErrIncompatiblePasswordHashVersion = errors.New("crypto: incompatible version of argon2")
    28  )
    29  
    30  // DefaultHashPasswordParams provides some sane default parameters for hashing passwords.
    31  // You are encouraged to change the Memory, Iterations and Parallelism parameters
    32  // to values appropriate for the environment that your code will be running in.
    33  var DefaultHashPasswordParams = HashPasswordParams{
    34  	Memory:      64 * 1024,
    35  	Iterations:  3,
    36  	Parallelism: 2,
    37  	SaltLength:  KeySize512,
    38  	KeyLength:   KeySize512,
    39  }
    40  
    41  // HashPasswordParams describes the input parameters used by the Argon2id algorithm. The
    42  // Memory and Iterations parameters control the computational cost of hashing
    43  // the password. The higher these figures are, the greater the cost of generating
    44  // the hash and the longer the runtime. It also follows that the greater the cost
    45  // will be for any attacker trying to guess the password. If the code is running
    46  // on a machine with multiple cores, then you can decrease the runtime without
    47  // reducing the cost by increasing the Parallelism parameter. This controls the
    48  // number of threads that the work is spread across. Important note: Changing the
    49  // value of the Parallelism parameter changes the hash output.
    50  //
    51  // For guidance and an outline process for choosing appropriate parameters see
    52  // https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4
    53  type HashPasswordParams struct {
    54  	// The amount of memory used by the algorithm (in kibibytes).
    55  	Memory uint32
    56  
    57  	// The number of iterations over the memory.
    58  	Iterations uint32
    59  
    60  	// The number of threads (or lanes) used by the algorithm.
    61  	Parallelism uint8
    62  
    63  	// Length of the random salt. 16 bytes is recommended for password hashing.
    64  	SaltLength uint32
    65  
    66  	// Length of the generated key. 32 bytes or more is recommended.
    67  	KeyLength uint32
    68  }
    69  
    70  // HashPassword returns a Argon2id hash of a plain-text password using the
    71  // provided algorithm parameters. The returned hash follows the format used by
    72  // the Argon2 reference C implementation and contains the base64-encoded Argon2id d
    73  // derived key prefixed by the salt and parameters. It looks like this:
    74  //
    75  //	$argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
    76  func HashPassword(password []byte, params HashPasswordParams) (hash string, err error) {
    77  	salt, err := RandBytes(uint64(params.SaltLength))
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  
    82  	key := argon2.IDKey(password, salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
    83  
    84  	b64Salt := base64.RawStdEncoding.EncodeToString(salt)
    85  	b64Key := base64.RawStdEncoding.EncodeToString(key)
    86  
    87  	hash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64Salt, b64Key)
    88  	return hash, nil
    89  }
    90  
    91  // VerifyPasswordHash performs a constant-time comparison between a
    92  // plain-text password and Argon2id hash, using the parameters and salt
    93  // contained in the hash. It returns true if they match, otherwise it returns
    94  // false.
    95  func VerifyPasswordHash(password []byte, hash string) bool {
    96  	params, salt, key, err := decodePasswordHash(hash)
    97  	if err != nil {
    98  		return false
    99  	}
   100  
   101  	otherKey := argon2.IDKey(password, salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
   102  
   103  	keyLen := int32(len(key))
   104  	otherKeyLen := int32(len(otherKey))
   105  
   106  	if subtle.ConstantTimeEq(keyLen, otherKeyLen) == 0 {
   107  		return false
   108  	}
   109  	if subtle.ConstantTimeCompare(key, otherKey) == 1 {
   110  		return true
   111  	}
   112  	return false
   113  }
   114  
   115  func decodePasswordHash(hash string) (params *HashPasswordParams, salt, key []byte, err error) {
   116  	vals := strings.Split(hash, "$")
   117  	if len(vals) != 6 {
   118  		return nil, nil, nil, ErrInvalidPasswordHash
   119  	}
   120  
   121  	var version int
   122  	_, err = fmt.Sscanf(vals[2], "v=%d", &version)
   123  	if err != nil {
   124  		return nil, nil, nil, err
   125  	}
   126  	if version != argon2.Version {
   127  		return nil, nil, nil, ErrIncompatiblePasswordHashVersion
   128  	}
   129  
   130  	params = &HashPasswordParams{}
   131  	_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &params.Memory, &params.Iterations, &params.Parallelism)
   132  	if err != nil {
   133  		return nil, nil, nil, err
   134  	}
   135  
   136  	salt, err = base64.RawStdEncoding.DecodeString(vals[4])
   137  	if err != nil {
   138  		return nil, nil, nil, err
   139  	}
   140  	params.SaltLength = uint32(len(salt))
   141  
   142  	key, err = base64.RawStdEncoding.DecodeString(vals[5])
   143  	if err != nil {
   144  		return nil, nil, nil, err
   145  	}
   146  	params.KeyLength = uint32(len(key))
   147  
   148  	return params, salt, key, nil
   149  }