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", ¶ms.Memory, ¶ms.Iterations, ¶ms.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 }